mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-05-08 17:25:01 -04:00
break everything
This commit is contained in:
parent
e898074387
commit
29210c89d2
121 changed files with 2892 additions and 2608 deletions
55
lib/old_to_refactor/components/account_bubble.dart
Normal file
55
lib/old_to_refactor/components/account_bubble.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:circular_profile_avatar/circular_profile_avatar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../entities/local_account.dart';
|
||||
import '../providers/logins.dart';
|
||||
|
||||
class AccountBubble extends ConsumerWidget {
|
||||
const AccountBubble({required this.account, super.key});
|
||||
final LocalAccount account;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
final logins = ref.watch(loginsProvider);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: CircularProfileAvatar('',
|
||||
child: Container(color: Theme.of(context).disabledColor))),
|
||||
const Expanded(child: Text('Placeholder'))
|
||||
]));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<LocalAccount>('account', account));
|
||||
}
|
||||
}
|
||||
|
||||
class AddAccountBubble extends ConsumerWidget {
|
||||
const AddAccountBubble({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
final logins = ref.watch(loginsProvider);
|
||||
|
||||
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
CircularProfileAvatar('',
|
||||
borderWidth: 4,
|
||||
borderColor: Theme.of(context).unselectedWidgetColor,
|
||||
child: Container(
|
||||
color: Colors.blue, child: const Icon(Icons.add, size: 50))),
|
||||
const Text('Add Account').paddingLTRB(0, 4, 0, 0)
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class BottomSheetActionButton extends ConsumerStatefulWidget {
|
||||
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 ConsumerState<BottomSheetActionButton> {
|
||||
bool _showFab = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
//
|
||||
return _showFab
|
||||
? FloatingActionButton(
|
||||
elevation: 0,
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
205
lib/old_to_refactor/components/chat_component.dart
Normal file
205
lib/old_to_refactor/components/chat_component.dart
Normal file
|
@ -0,0 +1,205 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../providers/account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../providers/conversation.dart';
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_init.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
|
||||
class ChatComponent extends ConsumerStatefulWidget {
|
||||
const ChatComponent(
|
||||
{required this.activeAccountInfo,
|
||||
required this.activeChat,
|
||||
required this.activeChatContact,
|
||||
super.key});
|
||||
|
||||
final ActiveAccountInfo activeAccountInfo;
|
||||
final TypedKey activeChat;
|
||||
final proto.Contact activeChatContact;
|
||||
|
||||
@override
|
||||
ChatComponentState createState() => ChatComponentState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<ActiveAccountInfo>(
|
||||
'activeAccountInfo', activeAccountInfo))
|
||||
..add(DiagnosticsProperty<TypedKey>('activeChat', activeChat))
|
||||
..add(DiagnosticsProperty<proto.Contact>(
|
||||
'activeChatContact', activeChatContact));
|
||||
}
|
||||
}
|
||||
|
||||
class ChatComponentState extends ConsumerState<ChatComponent> {
|
||||
final _unfocusNode = FocusNode();
|
||||
late final types.User _localUser;
|
||||
late final types.User _remoteUser;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_localUser = types.User(
|
||||
id: widget.activeAccountInfo.localAccount.identityMaster
|
||||
.identityPublicTypedKey()
|
||||
.toString(),
|
||||
firstName: widget.activeAccountInfo.account.profile.name,
|
||||
);
|
||||
_remoteUser = types.User(
|
||||
id: proto.TypedKeyProto.fromProto(
|
||||
widget.activeChatContact.identityPublicKey)
|
||||
.toString(),
|
||||
firstName: widget.activeChatContact.remoteProfile.name);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
types.Message protoMessageToMessage(proto.Message message) {
|
||||
final isLocal = message.author ==
|
||||
widget.activeAccountInfo.localAccount.identityMaster
|
||||
.identityPublicTypedKey()
|
||||
.toProto();
|
||||
|
||||
final textMessage = types.TextMessage(
|
||||
author: isLocal ? _localUser : _remoteUser,
|
||||
createdAt: (message.timestamp ~/ 1000).toInt(),
|
||||
id: message.timestamp.toString(),
|
||||
text: message.text,
|
||||
);
|
||||
return textMessage;
|
||||
}
|
||||
|
||||
Future<void> _addMessage(proto.Message protoMessage) async {
|
||||
if (protoMessage.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final message = protoMessageToMessage(protoMessage);
|
||||
|
||||
// setState(() {
|
||||
// _messages.insert(0, message);
|
||||
// });
|
||||
|
||||
// Now add the message to the conversation messages
|
||||
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
widget.activeChatContact.localConversationRecordKey);
|
||||
final remoteIdentityPublicKey = proto.TypedKeyProto.fromProto(
|
||||
widget.activeChatContact.identityPublicKey);
|
||||
|
||||
await addLocalConversationMessage(
|
||||
activeAccountInfo: widget.activeAccountInfo,
|
||||
localConversationRecordKey: localConversationRecordKey,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||
message: protoMessage);
|
||||
|
||||
ref.invalidate(activeConversationMessagesProvider);
|
||||
}
|
||||
|
||||
Future<void> _handleSendPressed(types.PartialText message) async {
|
||||
final protoMessage = proto.Message()
|
||||
..author = widget.activeAccountInfo.localAccount.identityMaster
|
||||
.identityPublicTypedKey()
|
||||
.toProto()
|
||||
..timestamp = (await eventualVeilid.future).now().toInt64()
|
||||
..text = message.text;
|
||||
//..signature = signature;
|
||||
|
||||
await _addMessage(protoMessage);
|
||||
}
|
||||
|
||||
void _handleAttachmentPressed() {
|
||||
//
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final chatTheme = makeChatTheme(scale, textTheme);
|
||||
final contactName = widget.activeChatContact.editedProfile.name;
|
||||
|
||||
final protoMessages =
|
||||
ref.watch(activeConversationMessagesProvider).asData?.value;
|
||||
if (protoMessages == null) {
|
||||
return waitingPage(context);
|
||||
}
|
||||
final messages = <types.Message>[];
|
||||
for (final protoMessage in protoMessages) {
|
||||
final message = protoMessageToMessage(protoMessage);
|
||||
messages.insert(0, message);
|
||||
}
|
||||
|
||||
return DefaultTextStyle(
|
||||
style: textTheme.bodySmall!,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
),
|
||||
child: Row(children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.fromSTEB(
|
||||
16, 0, 16, 0),
|
||||
child: Text(contactName,
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.titleMedium),
|
||||
)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
ref.read(activeChatStateProvider.notifier).state =
|
||||
null;
|
||||
}).paddingLTRB(16, 0, 16, 0)
|
||||
]),
|
||||
),
|
||||
Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(),
|
||||
child: Chat(
|
||||
theme: chatTheme,
|
||||
messages: messages,
|
||||
//onAttachmentPressed: _handleAttachmentPressed,
|
||||
//onMessageTap: _handleMessageTap,
|
||||
//onPreviewDataFetched: _handlePreviewDataFetched,
|
||||
|
||||
onSendPressed: (message) {
|
||||
unawaited(_handleSendPressed(message));
|
||||
},
|
||||
//showUserAvatars: false,
|
||||
//showUserNames: true,
|
||||
user: _localUser,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../providers/account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../theme/theme.dart';
|
||||
|
||||
class ChatSingleContactItemWidget extends ConsumerWidget {
|
||||
const ChatSingleContactItemWidget({required this.contact, super.key});
|
||||
|
||||
final proto.Contact contact;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
final activeChat = ref.watch(activeChatStateProvider);
|
||||
final remoteConversationRecordKey =
|
||||
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
|
||||
final selected = activeChat == remoteConversationRecordKey;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(0, 4, 0, 0),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.tertiaryScale.subtleBorder,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)),
|
||||
child: Slidable(
|
||||
key: ObjectKey(contact),
|
||||
endActionPane: ActionPane(
|
||||
motion: const DrawerMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (context) async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
await deleteChat(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteConversationRecordKey:
|
||||
remoteConversationRecordKey);
|
||||
ref.invalidate(fetchChatListProvider);
|
||||
}
|
||||
},
|
||||
backgroundColor: scale.tertiaryScale.background,
|
||||
foregroundColor: scale.tertiaryScale.text,
|
||||
icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
padding: const EdgeInsets.all(2)),
|
||||
// SlidableAction(
|
||||
// onPressed: (context) => (),
|
||||
// backgroundColor: scale.secondaryScale.background,
|
||||
// foregroundColor: scale.secondaryScale.text,
|
||||
// icon: Icons.edit,
|
||||
// label: 'Edit',
|
||||
// ),
|
||||
],
|
||||
),
|
||||
|
||||
// The child of the Slidable is what the user sees when the
|
||||
// component is not dragged.
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
ref.read(activeChatStateProvider.notifier).state =
|
||||
remoteConversationRecordKey;
|
||||
ref.invalidate(fetchChatListProvider);
|
||||
},
|
||||
title: Text(contact.editedProfile.name),
|
||||
|
||||
/// xxx show last message here
|
||||
subtitle: (contact.editedProfile.pronouns.isNotEmpty)
|
||||
? Text(contact.editedProfile.pronouns)
|
||||
: null,
|
||||
iconColor: scale.tertiaryScale.background,
|
||||
textColor: scale.tertiaryScale.text,
|
||||
selected: selected,
|
||||
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
|
||||
leading: const Icon(Icons.chat))));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:searchable_listview/searchable_listview.dart';
|
||||
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../tools/tools.dart';
|
||||
import 'chat_single_contact_item_widget.dart';
|
||||
import 'empty_chat_list_widget.dart';
|
||||
|
||||
class ChatSingleContactListWidget extends ConsumerWidget {
|
||||
ChatSingleContactListWidget(
|
||||
{required IList<proto.Contact> contactList,
|
||||
required this.chatList,
|
||||
super.key})
|
||||
: contactMap = IMap.fromIterable(contactList,
|
||||
keyMapper: (c) => c.remoteConversationRecordKey,
|
||||
valueMapper: (c) => c);
|
||||
|
||||
final IMap<proto.TypedKey, proto.Contact> contactMap;
|
||||
final IList<proto.Chat> chatList;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return SizedBox.expand(
|
||||
child: styledTitleContainer(
|
||||
context: context,
|
||||
title: translate('chat_list.chats'),
|
||||
child: SizedBox.expand(
|
||||
child: (chatList.isEmpty)
|
||||
? const EmptyChatListWidget()
|
||||
: SearchableList<proto.Chat>(
|
||||
autoFocusOnSearch: false,
|
||||
initialList: chatList.toList(),
|
||||
builder: (l, i, c) {
|
||||
final contact =
|
||||
contactMap[c.remoteConversationKey];
|
||||
if (contact == null) {
|
||||
return const Text('...');
|
||||
}
|
||||
return ChatSingleContactItemWidget(
|
||||
contact: contact);
|
||||
},
|
||||
filter: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
return chatList.where((c) {
|
||||
final contact =
|
||||
contactMap[c.remoteConversationKey];
|
||||
if (contact == null) {
|
||||
return false;
|
||||
}
|
||||
return contact.editedProfile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.editedProfile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue);
|
||||
}).toList();
|
||||
},
|
||||
spaceBetweenSearchAndList: 4,
|
||||
inputDecoration: InputDecoration(
|
||||
labelText: translate('chat_list.search'),
|
||||
contentPadding: const EdgeInsets.all(2),
|
||||
fillColor: scale.primaryScale.text,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: scale.primaryScale.hoverBorder,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
).paddingAll(8))))
|
||||
.paddingLTRB(8, 8, 8, 65);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<IMap<proto.TypedKey, proto.Contact>>(
|
||||
'contactMap', contactMap))
|
||||
..add(IterableProperty<proto.Chat>('chatList', chatList));
|
||||
}
|
||||
}
|
152
lib/old_to_refactor/components/contact_invitation_display.dart
Normal file
152
lib/old_to_refactor/components/contact_invitation_display.dart
Normal file
|
@ -0,0 +1,152 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:basic_utils/basic_utils.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
|
||||
class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
|
||||
const ContactInvitationDisplayDialog({
|
||||
required this.name,
|
||||
required this.message,
|
||||
required this.generator,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String message;
|
||||
final FutureOr<Uint8List> generator;
|
||||
|
||||
@override
|
||||
ContactInvitationDisplayDialogState createState() =>
|
||||
ContactInvitationDisplayDialogState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('name', name))
|
||||
..add(StringProperty('message', message))
|
||||
..add(DiagnosticsProperty<FutureOr<Uint8List>?>('generator', generator));
|
||||
}
|
||||
}
|
||||
|
||||
class ContactInvitationDisplayDialogState
|
||||
extends ConsumerState<ContactInvitationDisplayDialog> {
|
||||
final focusNode = FocusNode();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
late final AutoDisposeFutureProvider<Uint8List?> _generateFutureProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_generateFutureProvider =
|
||||
AutoDisposeFutureProvider<Uint8List>((ref) async => widget.generator);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String makeTextInvite(String message, Uint8List data) {
|
||||
final invite = StringUtils.addCharAtPosition(
|
||||
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';
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
final signedContactInvitationBytesV = ref.watch(_generateFutureProvider);
|
||||
final cardsize =
|
||||
min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400);
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: cardsize,
|
||||
maxWidth: cardsize,
|
||||
minHeight: cardsize,
|
||||
maxHeight: cardsize),
|
||||
child: signedContactInvitationBytesV.when(
|
||||
loading: () => buildProgressIndicator(context),
|
||||
data: (data) {
|
||||
if (data == null) {
|
||||
Navigator.of(context).pop();
|
||||
return const Text('');
|
||||
}
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: Column(children: [
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate(
|
||||
'send_invite_dialog.contact_invitation'),
|
||||
style: textTheme.headlineSmall!
|
||||
.copyWith(color: Colors.black)))
|
||||
.paddingAll(8),
|
||||
FittedBox(
|
||||
child: QrImageView.withQr(
|
||||
size: 300,
|
||||
qr: QrCode.fromUint8List(
|
||||
data: data,
|
||||
errorCorrectLevel:
|
||||
QrErrorCorrectLevel.L)))
|
||||
.expanded(),
|
||||
Text(widget.message,
|
||||
softWrap: true,
|
||||
style: textTheme.labelLarge!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
label: Text(
|
||||
translate('send_invite_dialog.copy_invitation')),
|
||||
onPressed: () async {
|
||||
showInfoToast(
|
||||
context,
|
||||
translate(
|
||||
'send_invite_dialog.invitation_copied'));
|
||||
await Clipboard.setData(ClipboardData(
|
||||
text: makeTextInvite(widget.message, data)));
|
||||
},
|
||||
).paddingAll(16),
|
||||
]));
|
||||
},
|
||||
error: (e, s) {
|
||||
Navigator.of(context).pop();
|
||||
showErrorToast(context,
|
||||
translate('send_invite_dialog.failed_to_generate'));
|
||||
return const Text('');
|
||||
})));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
|
||||
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../providers/account.dart';
|
||||
import '../providers/contact_invite.dart';
|
||||
import '../tools/tools.dart';
|
||||
import 'contact_invitation_display.dart';
|
||||
|
||||
class ContactInvitationItemWidget extends ConsumerWidget {
|
||||
const ContactInvitationItemWidget(
|
||||
{required this.contactInvitationRecord, super.key});
|
||||
|
||||
final proto.ContactInvitationRecord contactInvitationRecord;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<proto.ContactInvitationRecord>(
|
||||
'contactInvitationRecord', contactInvitationRecord));
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.tertiaryScale.subtleBorder,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)),
|
||||
child: Slidable(
|
||||
// Specify a key if the Slidable is dismissible.
|
||||
key: ObjectKey(contactInvitationRecord),
|
||||
endActionPane: ActionPane(
|
||||
// A motion is a widget used to control how the pane animates.
|
||||
motion: const DrawerMotion(),
|
||||
|
||||
// A pane can dismiss the Slidable.
|
||||
//dismissible: DismissiblePane(onDismissed: () {}),
|
||||
|
||||
// All actions are defined in the children parameter.
|
||||
children: [
|
||||
// A SlidableAction can have an icon and/or a label.
|
||||
SlidableAction(
|
||||
onPressed: (context) async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
await deleteContactInvitation(
|
||||
accepted: false,
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
contactInvitationRecord: contactInvitationRecord);
|
||||
ref.invalidate(fetchContactInvitationRecordsProvider);
|
||||
}
|
||||
},
|
||||
backgroundColor: scale.tertiaryScale.background,
|
||||
foregroundColor: scale.tertiaryScale.text,
|
||||
icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
padding: const EdgeInsets.all(2)),
|
||||
],
|
||||
),
|
||||
|
||||
// startActionPane: ActionPane(
|
||||
// motion: const DrawerMotion(),
|
||||
// children: [
|
||||
// SlidableAction(
|
||||
// // An action can be bigger than the others.
|
||||
// flex: 2,
|
||||
// onPressed: (context) => (),
|
||||
// backgroundColor: Color(0xFF7BC043),
|
||||
// foregroundColor: Colors.white,
|
||||
// icon: Icons.archive,
|
||||
// label: 'Archive',
|
||||
// ),
|
||||
// SlidableAction(
|
||||
// onPressed: (context) => (),
|
||||
// backgroundColor: Color(0xFF0392CF),
|
||||
// foregroundColor: Colors.white,
|
||||
// icon: Icons.save,
|
||||
// label: 'Save',
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
|
||||
// The child of the Slidable is what the user sees when the
|
||||
// component is not dragged.
|
||||
child: ListTile(
|
||||
//title: Text(translate('contact_list.invitation')),
|
||||
onTap: () async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ContactInvitationDisplayDialog(
|
||||
name: activeAccountInfo.localAccount.name,
|
||||
message: contactInvitationRecord.message,
|
||||
generator: Uint8List.fromList(
|
||||
contactInvitationRecord.invitation),
|
||||
));
|
||||
}
|
||||
},
|
||||
title: Text(
|
||||
contactInvitationRecord.message.isEmpty
|
||||
? translate('contact_list.invitation')
|
||||
: contactInvitationRecord.message,
|
||||
softWrap: true,
|
||||
),
|
||||
iconColor: scale.tertiaryScale.background,
|
||||
textColor: scale.tertiaryScale.text,
|
||||
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
|
||||
leading: const Icon(Icons.person_add))));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
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_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../tools/tools.dart';
|
||||
import 'contact_invitation_item_widget.dart';
|
||||
|
||||
class ContactInvitationListWidget extends ConsumerStatefulWidget {
|
||||
const ContactInvitationListWidget({
|
||||
required this.contactInvitationRecordList,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final IList<proto.ContactInvitationRecord> contactInvitationRecordList;
|
||||
|
||||
@override
|
||||
ContactInvitationListWidgetState createState() =>
|
||||
ContactInvitationListWidgetState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IterableProperty<proto.ContactInvitationRecord>(
|
||||
'contactInvitationRecordList', contactInvitationRecordList));
|
||||
}
|
||||
}
|
||||
|
||||
class ContactInvitationListWidgetState
|
||||
extends ConsumerState<ContactInvitationListWidget> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(4, 0, 4, 4),
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
)),
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.primaryScale.subtleBackground,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
)),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: widget.contactInvitationRecordList.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < 0 ||
|
||||
index >= widget.contactInvitationRecordList.length) {
|
||||
return null;
|
||||
}
|
||||
return ContactInvitationItemWidget(
|
||||
contactInvitationRecord:
|
||||
widget.contactInvitationRecordList[index],
|
||||
key: ObjectKey(widget.contactInvitationRecordList[index]))
|
||||
.paddingLTRB(4, 2, 4, 2);
|
||||
},
|
||||
findChildIndexCallback: (key) {
|
||||
final index = widget.contactInvitationRecordList.indexOf(
|
||||
(key as ObjectKey).value! as proto.ContactInvitationRecord);
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
).paddingLTRB(4, 6, 4, 6)),
|
||||
);
|
||||
}
|
||||
}
|
122
lib/old_to_refactor/components/contact_item_widget.dart
Normal file
122
lib/old_to_refactor/components/contact_item_widget.dart
Normal file
|
@ -0,0 +1,122 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../pages/main_pager/main_pager.dart';
|
||||
import '../providers/account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../providers/contact.dart';
|
||||
import '../theme/theme.dart';
|
||||
|
||||
class ContactItemWidget extends ConsumerWidget {
|
||||
const ContactItemWidget({required this.contact, super.key});
|
||||
|
||||
final proto.Contact contact;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
final remoteConversationKey =
|
||||
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(0, 4, 0, 0),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.tertiaryScale.subtleBorder,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)),
|
||||
child: Slidable(
|
||||
key: ObjectKey(contact),
|
||||
endActionPane: ActionPane(
|
||||
motion: const DrawerMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (context) async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
await deleteContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
contact: contact);
|
||||
ref
|
||||
..invalidate(fetchContactListProvider)
|
||||
..invalidate(fetchChatListProvider);
|
||||
}
|
||||
},
|
||||
backgroundColor: scale.tertiaryScale.background,
|
||||
foregroundColor: scale.tertiaryScale.text,
|
||||
icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
padding: const EdgeInsets.all(2)),
|
||||
// SlidableAction(
|
||||
// onPressed: (context) => (),
|
||||
// backgroundColor: scale.secondaryScale.background,
|
||||
// foregroundColor: scale.secondaryScale.text,
|
||||
// icon: Icons.edit,
|
||||
// label: 'Edit',
|
||||
// ),
|
||||
],
|
||||
),
|
||||
|
||||
// The child of the Slidable is what the user sees when the
|
||||
// component is not dragged.
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
// Start a chat
|
||||
await getOrCreateChatSingleContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteConversationRecordKey: remoteConversationKey);
|
||||
ref
|
||||
..invalidate(fetchContactListProvider)
|
||||
..invalidate(fetchChatListProvider);
|
||||
// Click over to chats
|
||||
if (context.mounted) {
|
||||
await MainPager.of(context)?.pageController.animateToPage(
|
||||
1,
|
||||
duration: 250.ms,
|
||||
curve: Curves.easeInOut);
|
||||
}
|
||||
}
|
||||
|
||||
// // ignore: use_build_context_synchronously
|
||||
// if (!context.mounted) {
|
||||
// return;
|
||||
// }
|
||||
// await showDialog<void>(
|
||||
// context: context,
|
||||
// builder: (context) => ContactInvitationDisplayDialog(
|
||||
// name: activeAccountInfo.localAccount.name,
|
||||
// message: contactInvitationRecord.message,
|
||||
// generator: Uint8List.fromList(
|
||||
// contactInvitationRecord.invitation),
|
||||
// ));
|
||||
// }
|
||||
},
|
||||
title: Text(contact.editedProfile.name),
|
||||
subtitle: (contact.editedProfile.pronouns.isNotEmpty)
|
||||
? Text(contact.editedProfile.pronouns)
|
||||
: null,
|
||||
iconColor: scale.tertiaryScale.background,
|
||||
textColor: scale.tertiaryScale.text,
|
||||
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
|
||||
leading: const Icon(Icons.person))));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
|
||||
}
|
||||
}
|
68
lib/old_to_refactor/components/contact_list_widget.dart
Normal file
68
lib/old_to_refactor/components/contact_list_widget.dart
Normal file
|
@ -0,0 +1,68 @@
|
|||
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:searchable_listview/searchable_listview.dart';
|
||||
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../tools/tools.dart';
|
||||
import 'contact_item_widget.dart';
|
||||
import 'empty_contact_list_widget.dart';
|
||||
|
||||
class ContactListWidget extends ConsumerWidget {
|
||||
const ContactListWidget({required this.contactList, super.key});
|
||||
final IList<proto.Contact> contactList;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IterableProperty<proto.Contact>('contactList', contactList));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return SizedBox.expand(
|
||||
child: styledTitleContainer(
|
||||
context: context,
|
||||
title: translate('contact_list.title'),
|
||||
child: SizedBox.expand(
|
||||
child: (contactList.isEmpty)
|
||||
? const EmptyContactListWidget()
|
||||
: SearchableList<proto.Contact>(
|
||||
autoFocusOnSearch: false,
|
||||
initialList: contactList.toList(),
|
||||
builder: (l, i, c) => ContactItemWidget(contact: c),
|
||||
filter: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
return contactList
|
||||
.where((element) =>
|
||||
element.editedProfile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
element.editedProfile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue))
|
||||
.toList();
|
||||
},
|
||||
spaceBetweenSearchAndList: 4,
|
||||
inputDecoration: InputDecoration(
|
||||
labelText: translate('contact_list.search'),
|
||||
contentPadding: const EdgeInsets.all(2),
|
||||
fillColor: scale.primaryScale.text,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: scale.primaryScale.hoverBorder,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
).paddingAll(8),
|
||||
))).paddingLTRB(8, 0, 8, 8);
|
||||
}
|
||||
}
|
18
lib/old_to_refactor/components/default_app_bar.dart
Normal file
18
lib/old_to_refactor/components/default_app_bar.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class DefaultAppBar extends AppBar {
|
||||
DefaultAppBar(
|
||||
{required super.title, super.key, Widget? leading, super.actions})
|
||||
: super(
|
||||
leading: leading ??
|
||||
Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(32),
|
||||
shape: BoxShape.circle),
|
||||
child:
|
||||
SvgPicture.asset('assets/images/vlogo.svg', height: 32)
|
||||
.paddingAll(4)));
|
||||
}
|
34
lib/old_to_refactor/components/empty_chat_list_widget.dart
Normal file
34
lib/old_to_refactor/components/empty_chat_list_widget.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class EmptyChatListWidget extends ConsumerWidget {
|
||||
const EmptyChatListWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat,
|
||||
color: scale.primaryScale.border,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
translate('chat_list.start_a_conversation'),
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: scale.primaryScale.border,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
36
lib/old_to_refactor/components/empty_chat_widget.dart
Normal file
36
lib/old_to_refactor/components/empty_chat_widget.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class EmptyChatWidget extends ConsumerWidget {
|
||||
const EmptyChatWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
//
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat,
|
||||
color: Theme.of(context).disabledColor,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
'Say Something',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class EmptyContactListWidget extends ConsumerWidget {
|
||||
const EmptyContactListWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_add_sharp,
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
translate('contact_list.invite_people'),
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
128
lib/old_to_refactor/components/enter_password.dart
Normal file
128
lib/old_to_refactor/components/enter_password.dart
Normal file
|
@ -0,0 +1,128 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class EnterPasswordDialog extends ConsumerStatefulWidget {
|
||||
const EnterPasswordDialog({
|
||||
this.matchPass,
|
||||
this.description,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String? matchPass;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
EnterPasswordDialogState createState() => EnterPasswordDialogState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('reenter', matchPass))
|
||||
..add(StringProperty('description', description));
|
||||
}
|
||||
}
|
||||
|
||||
class EnterPasswordDialogState extends ConsumerState<EnterPasswordDialog> {
|
||||
final passwordController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
bool _passwordVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
passwordController.dispose();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: scale.grayScale.subtleBackground,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.matchPass == null
|
||||
? translate('enter_password_dialog.enter_password')
|
||||
: translate('enter_password_dialog.reenter_password'),
|
||||
style: theme.textTheme.titleLarge,
|
||||
).paddingAll(16),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
enableSuggestions: false,
|
||||
obscureText:
|
||||
!_passwordVisible, //This will obscure text dynamically
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.singleLineFormatter
|
||||
],
|
||||
onSubmitted: (password) {
|
||||
Navigator.pop(context, password);
|
||||
},
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: widget.matchPass == null
|
||||
? null
|
||||
: Icon(Icons.check_circle,
|
||||
color: passwordController.text == widget.matchPass
|
||||
? scale.primaryScale.background
|
||||
: scale.grayScale.subtleBackground),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: scale.primaryScale.text,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
)).paddingAll(16),
|
||||
if (widget.description != null)
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: Text(
|
||||
widget.description!,
|
||||
textAlign: TextAlign.center,
|
||||
).paddingAll(16))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<TextEditingController>(
|
||||
'passwordController', passwordController))
|
||||
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
|
||||
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
|
||||
}
|
||||
}
|
142
lib/old_to_refactor/components/enter_pin.dart
Normal file
142
lib/old_to_refactor/components/enter_pin.dart
Normal file
|
@ -0,0 +1,142 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class EnterPinDialog extends ConsumerStatefulWidget {
|
||||
const EnterPinDialog({
|
||||
required this.reenter,
|
||||
required this.description,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool reenter;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
EnterPinDialogState createState() => EnterPinDialogState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('description', description))
|
||||
..add(DiagnosticsProperty<bool>('reenter', reenter));
|
||||
}
|
||||
}
|
||||
|
||||
class EnterPinDialogState extends ConsumerState<EnterPinDialog> {
|
||||
final pinController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pinController.dispose();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final focusedBorderColor = scale.primaryScale.hoverBorder;
|
||||
final fillColor = scale.primaryScale.elementBackground;
|
||||
final borderColor = scale.primaryScale.border;
|
||||
|
||||
final defaultPinTheme = PinTheme(
|
||||
width: 56,
|
||||
height: 60,
|
||||
textStyle: TextStyle(fontSize: 22, color: scale.primaryScale.text),
|
||||
decoration: BoxDecoration(
|
||||
color: fillColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
);
|
||||
|
||||
/// Optionally you can use form to validate the Pinput
|
||||
return Dialog(
|
||||
backgroundColor: scale.grayScale.subtleBackground,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
!widget.reenter
|
||||
? translate('enter_pin_dialog.enter_pin')
|
||||
: translate('enter_pin_dialog.reenter_pin'),
|
||||
style: theme.textTheme.titleLarge,
|
||||
).paddingAll(16),
|
||||
Directionality(
|
||||
// Specify direction if desired
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Pinput(
|
||||
controller: pinController,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
defaultPinTheme: defaultPinTheme,
|
||||
enableSuggestions: false,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
],
|
||||
hapticFeedbackType: HapticFeedbackType.lightImpact,
|
||||
onCompleted: (pin) {
|
||||
Navigator.pop(context, pin);
|
||||
},
|
||||
cursor: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 9),
|
||||
width: 22,
|
||||
height: 1,
|
||||
color: focusedBorderColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
focusedPinTheme: defaultPinTheme.copyWith(
|
||||
height: 68,
|
||||
width: 64,
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
),
|
||||
).paddingAll(16),
|
||||
),
|
||||
if (widget.description != null)
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: Text(
|
||||
widget.description!,
|
||||
textAlign: TextAlign.center,
|
||||
).paddingAll(16))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<TextEditingController>(
|
||||
'pinController', pinController))
|
||||
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
|
||||
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
|
||||
}
|
||||
}
|
343
lib/old_to_refactor/components/invite_dialog.dart
Normal file
343
lib/old_to_refactor/components/invite_dialog.dart
Normal file
|
@ -0,0 +1,343 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../entities/local_account.dart';
|
||||
import '../providers/account.dart';
|
||||
import '../providers/contact.dart';
|
||||
import '../providers/contact_invite.dart';
|
||||
import '../tools/tools.dart';
|
||||
import 'enter_password.dart';
|
||||
import 'enter_pin.dart';
|
||||
import 'profile_widget.dart';
|
||||
|
||||
class InviteDialog extends ConsumerStatefulWidget {
|
||||
const InviteDialog(
|
||||
{required this.onValidationCancelled,
|
||||
required this.onValidationSuccess,
|
||||
required this.onValidationFailed,
|
||||
required this.inviteControlIsValid,
|
||||
required this.buildInviteControl,
|
||||
super.key});
|
||||
|
||||
final void Function() onValidationCancelled;
|
||||
final void Function() onValidationSuccess;
|
||||
final void Function() onValidationFailed;
|
||||
final bool Function() inviteControlIsValid;
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
InviteDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData) buildInviteControl;
|
||||
|
||||
@override
|
||||
InviteDialogState createState() => InviteDialogState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(ObjectFlagProperty<void Function()>.has(
|
||||
'onValidationCancelled', onValidationCancelled))
|
||||
..add(ObjectFlagProperty<void Function()>.has(
|
||||
'onValidationSuccess', onValidationSuccess))
|
||||
..add(ObjectFlagProperty<void Function()>.has(
|
||||
'onValidationFailed', onValidationFailed))
|
||||
..add(ObjectFlagProperty<void Function()>.has(
|
||||
'inviteControlIsValid', inviteControlIsValid))
|
||||
..add(ObjectFlagProperty<
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
InviteDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData)>.has(
|
||||
'buildInviteControl', buildInviteControl));
|
||||
}
|
||||
}
|
||||
|
||||
class InviteDialogState extends ConsumerState<InviteDialog> {
|
||||
ValidContactInvitation? _validInvitation;
|
||||
bool _isValidating = false;
|
||||
bool _isAccepting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool get isValidating => _isValidating;
|
||||
bool get isAccepting => _isAccepting;
|
||||
|
||||
Future<void> _onAccept() async {
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
setState(() {
|
||||
_isAccepting = true;
|
||||
});
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
return;
|
||||
}
|
||||
final validInvitation = _validInvitation;
|
||||
if (validInvitation != null) {
|
||||
final acceptedContact =
|
||||
await acceptContactInvitation(activeAccountInfo, validInvitation);
|
||||
if (acceptedContact != null) {
|
||||
// initiator when accept is received will create
|
||||
// contact in the case of a 'note to self'
|
||||
final isSelf =
|
||||
activeAccountInfo.localAccount.identityMaster.identityPublicKey ==
|
||||
acceptedContact.remoteIdentity.identityPublicKey;
|
||||
if (!isSelf) {
|
||||
await createContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
profile: acceptedContact.profile,
|
||||
remoteIdentity: acceptedContact.remoteIdentity,
|
||||
remoteConversationRecordKey:
|
||||
acceptedContact.remoteConversationRecordKey,
|
||||
localConversationRecordKey:
|
||||
acceptedContact.localConversationRecordKey,
|
||||
);
|
||||
}
|
||||
ref
|
||||
..invalidate(fetchContactInvitationRecordsProvider)
|
||||
..invalidate(fetchContactListProvider);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
showErrorToast(context, 'invite_dialog.failed_to_accept');
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
Future<void> _onReject() async {
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
setState(() {
|
||||
_isAccepting = true;
|
||||
});
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
return;
|
||||
}
|
||||
final validInvitation = _validInvitation;
|
||||
if (validInvitation != null) {
|
||||
if (await rejectContactInvitation(activeAccountInfo, validInvitation)) {
|
||||
// do nothing right now
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
showErrorToast(context, 'invite_dialog.failed_to_reject');
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
Future<void> _validateInviteData({
|
||||
required Uint8List inviteData,
|
||||
}) async {
|
||||
try {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final contactInvitationRecords =
|
||||
await ref.read(fetchContactInvitationRecordsProvider.future);
|
||||
|
||||
setState(() {
|
||||
_isValidating = true;
|
||||
_validInvitation = null;
|
||||
});
|
||||
final validatedContactInvitation = await validateContactInvitation(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
contactInvitationRecords: contactInvitationRecords,
|
||||
inviteData: inviteData,
|
||||
getEncryptionKeyCallback:
|
||||
(cs, encryptionKeyType, encryptedSecret) async {
|
||||
String encryptionKey;
|
||||
switch (encryptionKeyType) {
|
||||
case EncryptionKeyType.none:
|
||||
encryptionKey = '';
|
||||
case EncryptionKeyType.pin:
|
||||
final description =
|
||||
translate('invite_dialog.protected_with_pin');
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
final pin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EnterPinDialog(
|
||||
reenter: false, description: description));
|
||||
if (pin == null) {
|
||||
return null;
|
||||
}
|
||||
encryptionKey = pin;
|
||||
case EncryptionKeyType.password:
|
||||
final description =
|
||||
translate('invite_dialog.protected_with_password');
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
final password = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
EnterPasswordDialog(description: description));
|
||||
if (password == null) {
|
||||
return null;
|
||||
}
|
||||
encryptionKey = password;
|
||||
}
|
||||
return decryptSecretFromBytes(
|
||||
secretBytes: encryptedSecret,
|
||||
cryptoKind: cs.kind(),
|
||||
encryptionKeyType: encryptionKeyType,
|
||||
encryptionKey: encryptionKey);
|
||||
});
|
||||
|
||||
// Check if validation was cancelled
|
||||
if (validatedContactInvitation == null) {
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
widget.onValidationCancelled();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify expiration
|
||||
// xxx
|
||||
|
||||
setState(() {
|
||||
widget.onValidationSuccess();
|
||||
_isValidating = false;
|
||||
_validInvitation = validatedContactInvitation;
|
||||
});
|
||||
} on ContactInviteInvalidKeyException catch (e) {
|
||||
String errorText;
|
||||
switch (e.type) {
|
||||
case EncryptionKeyType.none:
|
||||
errorText = translate('invite_dialog.invalid_invitation');
|
||||
case EncryptionKeyType.pin:
|
||||
errorText = translate('invite_dialog.invalid_pin');
|
||||
case EncryptionKeyType.password:
|
||||
errorText = translate('invite_dialog.invalid_password');
|
||||
}
|
||||
if (context.mounted) {
|
||||
showErrorToast(context, errorText);
|
||||
}
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
widget.onValidationFailed();
|
||||
});
|
||||
} on Exception catch (e) {
|
||||
log.debug('exception: $e', e);
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
widget.onValidationFailed();
|
||||
});
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
// final theme = Theme.of(context);
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final textTheme = theme.textTheme;
|
||||
// final height = MediaQuery.of(context).size.height;
|
||||
|
||||
if (_isAccepting) {
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: buildProgressIndicator(context).toCenter())
|
||||
.paddingAll(16);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
widget.buildInviteControl(context, this, _validateInviteData),
|
||||
if (_isValidating)
|
||||
Column(children: [
|
||||
Text(translate('invite_dialog.validating'))
|
||||
.paddingLTRB(0, 0, 0, 16),
|
||||
buildProgressIndicator(context).paddingAll(16),
|
||||
]).toCenter(),
|
||||
if (_validInvitation == null &&
|
||||
!_isValidating &&
|
||||
widget.inviteControlIsValid())
|
||||
Column(children: [
|
||||
Text(translate('invite_dialog.invalid_invitation')),
|
||||
const Icon(Icons.error)
|
||||
]).paddingAll(16).toCenter(),
|
||||
if (_validInvitation != null && !_isValidating)
|
||||
Column(children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 64),
|
||||
width: double.infinity,
|
||||
child: ProfileWidget(
|
||||
name: _validInvitation!
|
||||
.contactRequestPrivate.profile.name,
|
||||
pronouns: _validInvitation!
|
||||
.contactRequestPrivate.profile.pronouns,
|
||||
)).paddingLTRB(0, 0, 0, 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: Text(translate('button.accept')),
|
||||
onPressed: _onAccept,
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: Text(translate('button.reject')),
|
||||
onPressed: _onReject,
|
||||
)
|
||||
],
|
||||
),
|
||||
])
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<bool>('isValidating', isValidating))
|
||||
..add(DiagnosticsProperty<bool>('isAccepting', isAccepting));
|
||||
}
|
||||
}
|
35
lib/old_to_refactor/components/no_conversation_widget.dart
Normal file
35
lib/old_to_refactor/components/no_conversation_widget.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class NoContactWidget extends ConsumerWidget {
|
||||
const NoContactWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
//
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.emoji_people_outlined,
|
||||
color: Theme.of(context).disabledColor,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
'Choose A Conversation To Chat',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
131
lib/old_to_refactor/components/paste_invite_dialog.dart
Normal file
131
lib/old_to_refactor/components/paste_invite_dialog.dart
Normal file
|
@ -0,0 +1,131 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
import 'invite_dialog.dart';
|
||||
|
||||
class PasteInviteDialog extends ConsumerStatefulWidget {
|
||||
const PasteInviteDialog({super.key});
|
||||
|
||||
@override
|
||||
PasteInviteDialogState createState() => PasteInviteDialogState();
|
||||
|
||||
static Future<void> show(BuildContext context) async {
|
||||
await showStyledDialog<void>(
|
||||
context: context,
|
||||
title: translate('paste_invite_dialog.title'),
|
||||
child: const PasteInviteDialog());
|
||||
}
|
||||
}
|
||||
|
||||
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
||||
final _pasteTextController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _onPasteChanged(
|
||||
String text,
|
||||
Future<void> Function({
|
||||
required Uint8List inviteData,
|
||||
}) validateInviteData) async {
|
||||
final lines = text.split('\n');
|
||||
if (lines.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
var firstline =
|
||||
lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
|
||||
firstline += 1;
|
||||
|
||||
var lastline =
|
||||
lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
|
||||
if (lastline == -1) {
|
||||
lastline = lines.length;
|
||||
}
|
||||
if (lastline <= firstline) {
|
||||
return;
|
||||
}
|
||||
final inviteDataBase64 = lines
|
||||
.sublist(firstline, lastline)
|
||||
.join()
|
||||
.replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), '');
|
||||
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
||||
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
}
|
||||
|
||||
void onValidationCancelled() {
|
||||
_pasteTextController.clear();
|
||||
}
|
||||
|
||||
void onValidationSuccess() {
|
||||
//_pasteTextController.clear();
|
||||
}
|
||||
|
||||
void onValidationFailed() {
|
||||
_pasteTextController.clear();
|
||||
}
|
||||
|
||||
bool inviteControlIsValid() => _pasteTextController.text.isNotEmpty;
|
||||
|
||||
Widget buildInviteControl(
|
||||
BuildContext context,
|
||||
InviteDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
//final textTheme = theme.textTheme;
|
||||
//final height = MediaQuery.of(context).size.height;
|
||||
|
||||
final monoStyle = TextStyle(
|
||||
fontFamily: 'Source Code Pro',
|
||||
fontSize: 11,
|
||||
color: scale.primaryScale.text,
|
||||
);
|
||||
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(
|
||||
translate('paste_invite_dialog.paste_invite_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: TextField(
|
||||
enabled: !dialogState.isValidating,
|
||||
onChanged: (text) async =>
|
||||
_onPasteChanged(text, validateInviteData),
|
||||
style: monoStyle,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
controller: _pasteTextController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
||||
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n'
|
||||
'---- END VEILIDCHAT CONTACT INVITE -----\n',
|
||||
//labelText: translate('paste_invite_dialog.paste')
|
||||
),
|
||||
)).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return InviteDialog(
|
||||
onValidationCancelled: onValidationCancelled,
|
||||
onValidationSuccess: onValidationSuccess,
|
||||
onValidationFailed: onValidationFailed,
|
||||
inviteControlIsValid: inviteControlIsValid,
|
||||
buildInviteControl: buildInviteControl);
|
||||
}
|
||||
}
|
49
lib/old_to_refactor/components/profile_widget.dart
Normal file
49
lib/old_to_refactor/components/profile_widget.dart
Normal file
|
@ -0,0 +1,49 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class ProfileWidget extends ConsumerWidget {
|
||||
const ProfileWidget({
|
||||
required this.name,
|
||||
this.pronouns,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String? pronouns;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.primaryScale.border,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))),
|
||||
child: Column(children: [
|
||||
Text(
|
||||
name,
|
||||
style: textTheme.headlineSmall,
|
||||
textAlign: TextAlign.left,
|
||||
).paddingAll(4),
|
||||
if (pronouns != null && pronouns!.isNotEmpty)
|
||||
Text(pronouns!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('name', name))
|
||||
..add(StringProperty('pronouns', pronouns));
|
||||
}
|
||||
}
|
399
lib/old_to_refactor/components/scan_invite_dialog.dart
Normal file
399
lib/old_to_refactor/components/scan_invite_dialog.dart
Normal file
|
@ -0,0 +1,399 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
import 'invite_dialog.dart';
|
||||
|
||||
class BarcodeOverlay extends CustomPainter {
|
||||
BarcodeOverlay({
|
||||
required this.barcode,
|
||||
required this.arguments,
|
||||
required this.boxFit,
|
||||
required this.capture,
|
||||
});
|
||||
|
||||
final BarcodeCapture capture;
|
||||
final Barcode barcode;
|
||||
final MobileScannerArguments arguments;
|
||||
final BoxFit boxFit;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (barcode.corners == null) {
|
||||
return;
|
||||
}
|
||||
final adjustedSize = applyBoxFit(boxFit, arguments.size, size);
|
||||
|
||||
var verticalPadding = size.height - adjustedSize.destination.height;
|
||||
var horizontalPadding = size.width - adjustedSize.destination.width;
|
||||
if (verticalPadding > 0) {
|
||||
verticalPadding = verticalPadding / 2;
|
||||
} else {
|
||||
verticalPadding = 0;
|
||||
}
|
||||
|
||||
if (horizontalPadding > 0) {
|
||||
horizontalPadding = horizontalPadding / 2;
|
||||
} else {
|
||||
horizontalPadding = 0;
|
||||
}
|
||||
|
||||
final ratioWidth =
|
||||
(Platform.isIOS ? capture.width! : arguments.size.width) /
|
||||
adjustedSize.destination.width;
|
||||
final ratioHeight =
|
||||
(Platform.isIOS ? capture.height! : arguments.size.height) /
|
||||
adjustedSize.destination.height;
|
||||
|
||||
final adjustedOffset = <Offset>[];
|
||||
for (final offset in barcode.corners!) {
|
||||
adjustedOffset.add(
|
||||
Offset(
|
||||
offset.dx / ratioWidth + horizontalPadding,
|
||||
offset.dy / ratioHeight + verticalPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
final cutoutPath = Path()..addPolygon(adjustedOffset, true);
|
||||
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.red.withOpacity(0.3)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.dstOut;
|
||||
|
||||
canvas.drawPath(cutoutPath, backgroundPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class ScannerOverlay extends CustomPainter {
|
||||
ScannerOverlay(this.scanWindow);
|
||||
|
||||
final Rect scanWindow;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final backgroundPath = Path()..addRect(Rect.largest);
|
||||
final cutoutPath = Path()..addRect(scanWindow);
|
||||
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.5)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.dstOut;
|
||||
|
||||
final backgroundWithCutout = Path.combine(
|
||||
PathOperation.difference,
|
||||
backgroundPath,
|
||||
cutoutPath,
|
||||
);
|
||||
canvas.drawPath(backgroundWithCutout, backgroundPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class ScanInviteDialog extends ConsumerStatefulWidget {
|
||||
const ScanInviteDialog({super.key});
|
||||
|
||||
@override
|
||||
ScanInviteDialogState createState() => ScanInviteDialogState();
|
||||
|
||||
static Future<void> show(BuildContext context) async {
|
||||
await showStyledDialog<void>(
|
||||
context: context,
|
||||
title: translate('scan_invite_dialog.title'),
|
||||
child: const ScanInviteDialog());
|
||||
}
|
||||
}
|
||||
|
||||
class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
|
||||
bool scanned = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void onValidationCancelled() {
|
||||
setState(() {
|
||||
scanned = false;
|
||||
});
|
||||
}
|
||||
|
||||
void onValidationSuccess() {}
|
||||
void onValidationFailed() {
|
||||
setState(() {
|
||||
scanned = false;
|
||||
});
|
||||
}
|
||||
|
||||
bool inviteControlIsValid() => false; // _pasteTextController.text.isNotEmpty;
|
||||
|
||||
Future<Uint8List?> scanQRImage(BuildContext context) async {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
//final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0);
|
||||
//final maxDialogHeight = windowSize.height - 64.0;
|
||||
|
||||
final scanWindow = Rect.fromCenter(
|
||||
center: MediaQuery.of(context).size.center(Offset.zero),
|
||||
width: 200,
|
||||
height: 200,
|
||||
);
|
||||
|
||||
final cameraController = MobileScannerController();
|
||||
try {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
MobileScanner(
|
||||
fit: BoxFit.contain,
|
||||
scanWindow: scanWindow,
|
||||
controller: cameraController,
|
||||
errorBuilder: (context, error, child) =>
|
||||
ScannerErrorWidget(error: error),
|
||||
onDetect: (c) {
|
||||
final barcode = c.barcodes.firstOrNull;
|
||||
|
||||
final barcodeBytes = barcode?.rawBytes;
|
||||
if (barcodeBytes != null) {
|
||||
cameraController.dispose();
|
||||
Navigator.pop(context, barcodeBytes);
|
||||
}
|
||||
}),
|
||||
CustomPaint(
|
||||
painter: ScannerOverlay(scanWindow),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
height: 100,
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: ValueListenableBuilder(
|
||||
valueListenable: cameraController.torchState,
|
||||
builder: (context, state, child) {
|
||||
switch (state) {
|
||||
case TorchState.off:
|
||||
return Icon(Icons.flash_off,
|
||||
color:
|
||||
scale.grayScale.subtleBackground);
|
||||
case TorchState.on:
|
||||
return Icon(Icons.flash_on,
|
||||
color: scale.primaryScale.background);
|
||||
}
|
||||
},
|
||||
),
|
||||
iconSize: 32,
|
||||
onPressed: cameraController.toggleTorch,
|
||||
),
|
||||
SizedBox(
|
||||
width: windowSize.width - 120,
|
||||
height: 50,
|
||||
child: FittedBox(
|
||||
child: Text(
|
||||
translate('scan_invite_dialog.instructions'),
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: ValueListenableBuilder(
|
||||
valueListenable:
|
||||
cameraController.cameraFacingState,
|
||||
builder: (context, state, child) {
|
||||
switch (state) {
|
||||
case CameraFacing.front:
|
||||
return const Icon(Icons.camera_front);
|
||||
case CameraFacing.back:
|
||||
return const Icon(Icons.camera_rear);
|
||||
}
|
||||
},
|
||||
),
|
||||
iconSize: 32,
|
||||
onPressed: cameraController.switchCamera,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.close,
|
||||
color: scale.grayScale.background),
|
||||
iconSize: 32,
|
||||
onPressed: () => {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((_) {
|
||||
cameraController.dispose();
|
||||
Navigator.pop(context, null);
|
||||
})
|
||||
})),
|
||||
],
|
||||
));
|
||||
} on MobileScannerException catch (e) {
|
||||
if (e.errorCode == MobileScannerErrorCode.permissionDenied) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invite_dialog.permission_error'));
|
||||
} else {
|
||||
showErrorToast(context, translate('scan_invite_dialog.error'));
|
||||
}
|
||||
} on Exception catch (_) {
|
||||
showErrorToast(context, translate('scan_invite_dialog.error'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Uint8List?> pasteQRImage(BuildContext context) async {
|
||||
final imageBytes = await Pasteboard.image;
|
||||
if (imageBytes == null) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(context, translate('scan_invite_dialog.not_an_image'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final image = img.decodeImage(imageBytes);
|
||||
if (image == null) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invite_dialog.could_not_decode_image'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final source = RGBLuminanceSource(
|
||||
image.width,
|
||||
image.height,
|
||||
image
|
||||
.convert(numChannels: 4)
|
||||
.getBytes(order: img.ChannelOrder.abgr)
|
||||
.buffer
|
||||
.asInt32List());
|
||||
final bitmap = BinaryBitmap(HybridBinarizer(source));
|
||||
|
||||
final reader = QRCodeReader();
|
||||
final result = reader.decode(bitmap);
|
||||
|
||||
final segs = result.resultMetadata[ResultMetadataType.byteSegments]!
|
||||
as List<Int8List>;
|
||||
return Uint8List.fromList(segs[0].toList());
|
||||
} on Exception catch (_) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invite_dialog.not_a_valid_qr_code'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildInviteControl(
|
||||
BuildContext context,
|
||||
InviteDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData) {
|
||||
//final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
//final textTheme = theme.textTheme;
|
||||
//final height = MediaQuery.of(context).size.height;
|
||||
|
||||
if (isiOS || isAndroid) {
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (!scanned)
|
||||
Text(
|
||||
translate('scan_invite_dialog.scan_qr_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
if (!scanned)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ElevatedButton(
|
||||
onPressed: dialogState.isValidating
|
||||
? null
|
||||
: () async {
|
||||
final inviteData = await scanQRImage(context);
|
||||
if (inviteData != null) {
|
||||
setState(() {
|
||||
scanned = true;
|
||||
});
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
}
|
||||
},
|
||||
child: Text(translate('scan_invite_dialog.scan'))),
|
||||
).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (!scanned)
|
||||
Text(
|
||||
translate('scan_invite_dialog.paste_qr_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
if (!scanned)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ElevatedButton(
|
||||
onPressed: dialogState.isValidating
|
||||
? null
|
||||
: () async {
|
||||
final inviteData = await pasteQRImage(context);
|
||||
if (inviteData != null) {
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
setState(() {
|
||||
scanned = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Text(translate('scan_invite_dialog.paste'))),
|
||||
).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return InviteDialog(
|
||||
onValidationCancelled: onValidationCancelled,
|
||||
onValidationSuccess: onValidationSuccess,
|
||||
onValidationFailed: onValidationFailed,
|
||||
inviteControlIsValid: inviteControlIsValid,
|
||||
buildInviteControl: buildInviteControl);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<bool>('scanned', scanned));
|
||||
}
|
||||
}
|
248
lib/old_to_refactor/components/send_invite_dialog.dart
Normal file
248
lib/old_to_refactor/components/send_invite_dialog.dart
Normal file
|
@ -0,0 +1,248 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../entities/local_account.dart';
|
||||
import '../providers/account.dart';
|
||||
import '../providers/contact_invite.dart';
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
import 'contact_invitation_display.dart';
|
||||
import 'enter_password.dart';
|
||||
import 'enter_pin.dart';
|
||||
|
||||
class SendInviteDialog extends ConsumerStatefulWidget {
|
||||
const SendInviteDialog({super.key});
|
||||
|
||||
@override
|
||||
SendInviteDialogState createState() => SendInviteDialogState();
|
||||
|
||||
static Future<void> show(BuildContext context) async {
|
||||
await showStyledDialog<void>(
|
||||
context: context,
|
||||
title: translate('send_invite_dialog.title'),
|
||||
child: const SendInviteDialog());
|
||||
}
|
||||
}
|
||||
|
||||
class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
|
||||
final _messageTextController = TextEditingController(
|
||||
text: translate('send_invite_dialog.connect_with_me'));
|
||||
|
||||
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
||||
String _encryptionKey = '';
|
||||
Timestamp? _expiration;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _onNoneEncryptionSelected(bool selected) async {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_encryptionKeyType = EncryptionKeyType.none;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onPinEncryptionSelected(bool selected) async {
|
||||
final description = translate('send_invite_dialog.pin_description');
|
||||
final pin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
EnterPinDialog(reenter: false, description: description));
|
||||
if (pin == null) {
|
||||
return;
|
||||
}
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
final matchpin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EnterPinDialog(
|
||||
reenter: true,
|
||||
description: description,
|
||||
));
|
||||
if (matchpin == null) {
|
||||
return;
|
||||
} else if (pin == matchpin) {
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.pin;
|
||||
_encryptionKey = pin;
|
||||
});
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
showErrorToast(
|
||||
context, translate('send_invite_dialog.pin_does_not_match'));
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.none;
|
||||
_encryptionKey = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPasswordEncryptionSelected(bool selected) async {
|
||||
final description = translate('send_invite_dialog.password_description');
|
||||
final password = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EnterPasswordDialog(description: description));
|
||||
if (password == null) {
|
||||
return;
|
||||
}
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
final matchpass = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EnterPasswordDialog(
|
||||
matchPass: password,
|
||||
description: description,
|
||||
));
|
||||
if (matchpass == null) {
|
||||
return;
|
||||
} else if (password == matchpass) {
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.password;
|
||||
_encryptionKey = password;
|
||||
});
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
showErrorToast(
|
||||
context, translate('send_invite_dialog.password_does_not_match'));
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.none;
|
||||
_encryptionKey = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onGenerateButtonPressed() async {
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
// Start generation
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
navigator.pop();
|
||||
return;
|
||||
}
|
||||
final generator = createContactInvitation(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
encryptionKeyType: _encryptionKeyType,
|
||||
encryptionKey: _encryptionKey,
|
||||
message: _messageTextController.text,
|
||||
expiration: _expiration);
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ContactInvitationDisplayDialog(
|
||||
name: activeAccountInfo.localAccount.name,
|
||||
message: _messageTextController.text,
|
||||
generator: generator,
|
||||
));
|
||||
// if (ret == null) {
|
||||
// return;
|
||||
// }
|
||||
ref.invalidate(fetchContactInvitationRecordsProvider);
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0);
|
||||
final maxDialogHeight = windowSize.height - 64.0;
|
||||
|
||||
final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
return ConstrainedBox(
|
||||
constraints:
|
||||
BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
translate('send_invite_dialog.message_to_contact'),
|
||||
).paddingAll(8),
|
||||
TextField(
|
||||
controller: _messageTextController,
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(128),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: translate('send_invite_dialog.enter_message_hint'),
|
||||
labelText: translate('send_invite_dialog.message')),
|
||||
).paddingAll(8),
|
||||
const SizedBox(height: 10),
|
||||
Text(translate('send_invite_dialog.protect_this_invitation'),
|
||||
style: textTheme.labelLarge)
|
||||
.paddingAll(8),
|
||||
Wrap(spacing: 5, children: [
|
||||
ChoiceChip(
|
||||
label: Text(translate('send_invite_dialog.unlocked')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.none,
|
||||
onSelected: _onNoneEncryptionSelected,
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(translate('send_invite_dialog.pin')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.pin,
|
||||
onSelected: _onPinEncryptionSelected,
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(translate('send_invite_dialog.password')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.password,
|
||||
onSelected: _onPasswordEncryptionSelected,
|
||||
)
|
||||
]).paddingAll(8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ElevatedButton(
|
||||
onPressed: _onGenerateButtonPressed,
|
||||
child: Text(
|
||||
translate('send_invite_dialog.generate'),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(translate('send_invite_dialog.note')).paddingAll(8),
|
||||
Text(
|
||||
translate('send_invite_dialog.note_text'),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).paddingAll(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<TextEditingController>(
|
||||
'messageTextController', _messageTextController));
|
||||
}
|
||||
}
|
66
lib/old_to_refactor/components/signal_strength_meter.dart
Normal file
66
lib/old_to_refactor/components/signal_strength_meter.dart
Normal file
|
@ -0,0 +1,66 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:signal_strength_indicator/signal_strength_indicator.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../providers/connection_state.dart';
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
|
||||
class SignalStrengthMeterWidget extends ConsumerWidget {
|
||||
const SignalStrengthMeterWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
const iconSize = 16.0;
|
||||
final connState = ref.watch(connectionStateProvider);
|
||||
|
||||
late final double value;
|
||||
late final Color color;
|
||||
late final Color inactiveColor;
|
||||
switch (connState.attachment.state) {
|
||||
case AttachmentState.detached:
|
||||
return Icon(Icons.signal_cellular_nodata,
|
||||
size: iconSize, color: scale.grayScale.text);
|
||||
case AttachmentState.detaching:
|
||||
return Icon(Icons.signal_cellular_off,
|
||||
size: iconSize, color: scale.grayScale.text);
|
||||
case AttachmentState.attaching:
|
||||
value = 0;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.attachedWeak:
|
||||
value = 1;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.attachedStrong:
|
||||
value = 2;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.attachedGood:
|
||||
value = 3;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.fullyAttached:
|
||||
value = 4;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.overAttached:
|
||||
value = 4;
|
||||
color = scale.secondaryScale.subtleText;
|
||||
}
|
||||
inactiveColor = scale.grayScale.subtleText;
|
||||
|
||||
return GestureDetector(
|
||||
onLongPress: () async {
|
||||
await context.push('/developer');
|
||||
},
|
||||
child: SignalStrengthIndicator.bars(
|
||||
value: value,
|
||||
activeColor: color,
|
||||
inactiveColor: inactiveColor,
|
||||
size: iconSize,
|
||||
barCount: 4,
|
||||
spacing: 1,
|
||||
));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue