more refactor

This commit is contained in:
Christien Rioux 2024-01-09 20:58:27 -05:00
parent b83aa3a64b
commit c7b541c643
45 changed files with 860 additions and 336 deletions

View File

@ -1,3 +1,4 @@
export 'cubit/cubit.dart';
export 'models/models.dart';
export 'repository/repository.dart';
export 'views/views.dart';

1
lib/chat/chat.dart Normal file
View File

@ -0,0 +1 @@
export 'views/views.dart';

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import '../../tools/tools.dart';
Widget buildChatComponent() {
// final contactList = ref.watch(fetchContactListProvider).asData?.value ??
// const IListConst([]);
// final activeChat = ref.watch(activeChatStateProvider);
// if (activeChat == null) {
// return const EmptyChatWidget();
// }
// final activeAccountInfo =
// ref.watch(fetchActiveAccountProvider).asData?.value;
// if (activeAccountInfo == null) {
// return const EmptyChatWidget();
// }
// final activeChatContactIdx = contactList.indexWhere(
// (c) =>
// proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
// activeChat,
// );
// if (activeChatContactIdx == -1) {
// ref.read(activeChatStateProvider.notifier).state = null;
// return const EmptyChatWidget();
// }
// final activeChatContact = contactList[activeChatContactIdx];
// return ChatComponent(
// activeAccountInfo: activeAccountInfo,
// activeChat: activeChat,
// activeChatContact: activeChatContact);
// }
return Builder(builder: waitingPage);
}

View File

@ -7,13 +7,13 @@ 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';
import '../../old_to_refactor/proto/proto.dart' as proto;
import '../../old_to_refactor/providers/account.dart';
import '../../old_to_refactor/providers/chat.dart';
import '../../old_to_refactor/providers/conversation.dart';
import '../../old_to_refactor/tools/tools.dart';
import '../../old_to_refactor/veilid_init.dart';
import '../../old_to_refactor/veilid_support/veilid_support.dart';
class ChatComponent extends ConsumerStatefulWidget {
const ChatComponent(

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class EmptyChatWidget extends StatelessWidget {
const EmptyChatWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(
BuildContext context,
) =>
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,
),
),
],
),
);
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
//XXX should rename this
class NoContactWidget extends StatelessWidget {
const NoContactWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(
BuildContext context,
) =>
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,
),
),
],
),
);
}

View File

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

View File

@ -1,15 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../tools/tools.dart';
import '../../theme/theme.dart';
class EmptyChatListWidget extends ConsumerWidget {
class EmptyChatListWidget extends StatelessWidget {
const EmptyChatListWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;

View File

View File

@ -0,0 +1 @@
export 'views/views.dart';

View File

@ -6,9 +6,9 @@ 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';
import '../old_to_refactor/tools/tools.dart';
import '../old_to_refactor/veilid_support/veilid_support.dart';
import 'views/invite_dialog.dart';
class PasteInviteDialog extends ConsumerStatefulWidget {
const PasteInviteDialog({super.key});

View File

@ -13,8 +13,8 @@ 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';
import '../old_to_refactor/tools/tools.dart';
import 'views/invite_dialog.dart';
class BarcodeOverlay extends CustomPainter {
BarcodeOverlay({

View File

@ -6,14 +6,13 @@ 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 'package:veilid_support/veilid_support.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import '../../tools/tools.dart';
class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
class ContactInvitationDisplayDialog extends StatefulWidget {
const ContactInvitationDisplayDialog({
required this.name,
required this.message,
@ -40,7 +39,7 @@ class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
}
class ContactInvitationDisplayDialogState
extends ConsumerState<ContactInvitationDisplayDialog> {
extends State<ContactInvitationDisplayDialog> {
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
late final AutoDisposeFutureProvider<Uint8List?> _generateFutureProvider;

View File

@ -1,15 +1,12 @@
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 '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import 'contact_invitation_display.dart';
class ContactInvitationItemWidget extends ConsumerWidget {
class ContactInvitationItemWidget extends StatelessWidget {
const ContactInvitationItemWidget(
{required this.contactInvitationRecord, super.key});
@ -24,7 +21,7 @@ class ContactInvitationItemWidget extends ConsumerWidget {
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;

View File

@ -2,13 +2,12 @@ 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 '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import 'contact_invitation_item_widget.dart';
class ContactInvitationListWidget extends ConsumerStatefulWidget {
class ContactInvitationListWidget extends StatefulWidget {
const ContactInvitationListWidget({
required this.contactInvitationRecordList,
super.key,
@ -28,7 +27,7 @@ class ContactInvitationListWidget extends ConsumerStatefulWidget {
}
class ContactInvitationListWidgetState
extends ConsumerState<ContactInvitationListWidget> {
extends State<ContactInvitationListWidget> {
final ScrollController _scrollController = ScrollController();
@override

View File

@ -3,19 +3,12 @@ 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';
import '../../account_manager/account_manager.dart';
import '../../tools/tools.dart';
class InviteDialog extends ConsumerStatefulWidget {
class InviteDialog extends StatefulWidget {
const InviteDialog(
{required this.onValidationCancelled,
required this.onValidationSuccess,
@ -58,7 +51,7 @@ class InviteDialog extends ConsumerStatefulWidget {
}
}
class InviteDialogState extends ConsumerState<InviteDialog> {
class InviteDialogState extends State<InviteDialog> {
ValidContactInvitation? _validInvitation;
bool _isValidating = false;
bool _isAccepting = false;

View File

@ -0,0 +1,65 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../theme/theme.dart';
import 'paste_invite_dialog.dart';
import 'scan_invite_dialog.dart';
import 'send_invite_dialog.dart';
Widget newContactInvitationBottomSheetBuilder(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (ke) {
if (ke.logicalKey == LogicalKeyboardKey.escape) {
Navigator.pop(context);
}
},
child: SizedBox(
height: 200,
child: Column(children: [
Text(translate('accounts_menu.invite_contact'),
style: textTheme.titleMedium)
.paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await SendInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.contact_page),
color: scale.primaryScale.background),
Text(translate('accounts_menu.create_invite'))
]),
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await ScanInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.qr_code_scanner),
color: scale.primaryScale.background),
Text(translate('accounts_menu.scan_invite'))
]),
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await PasteInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.paste),
color: scale.primaryScale.background),
Text(translate('accounts_menu.paste_invite'))
])
]).expanded()
])));
}

View 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_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import 'invite_dialog.dart';
class PasteInviteDialog extends StatefulWidget {
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 State<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);
}
}

View File

@ -0,0 +1,395 @@
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_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 '../../theme/theme.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) {
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 StatefulWidget {
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 State<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));
}
}

View File

@ -5,19 +5,14 @@ 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:veilid_support/veilid_support.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 '../../account_manager/account_manager.dart';
import '../../tools/tools.dart';
import 'contact_invitation_display.dart';
import 'enter_password.dart';
import 'enter_pin.dart';
class SendInviteDialog extends ConsumerStatefulWidget {
class SendInviteDialog extends StatefulWidget {
const SendInviteDialog({super.key});
@override
@ -31,7 +26,7 @@ class SendInviteDialog extends ConsumerStatefulWidget {
}
}
class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
class SendInviteDialogState extends State<SendInviteDialog> {
final _messageTextController = TextEditingController(
text: translate('send_invite_dialog.connect_with_me'));
@ -135,7 +130,8 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
final navigator = Navigator.of(context);
// Start generation
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
final activeAccountInfo =
await AccountRepository.instance.fetchActiveAccountInfo();
if (activeAccountInfo == null) {
navigator.pop();
return;

View File

@ -0,0 +1,8 @@
export 'contact_invitation_display.dart';
export 'contact_invitation_item_widget.dart';
export 'contact_invitation_list_widget.dart';
export 'invite_dialog.dart';
export 'new_contact_invitation_bottom_sheet.dart';
export 'paste_invite_dialog.dart';
export 'scan_invite_dialog.dart';
export 'send_invite_dialog.dart';

View File

@ -0,0 +1 @@
export 'views/views.dart';

View File

@ -1,24 +1,21 @@
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';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
class ContactItemWidget extends ConsumerWidget {
class ContactItemWidget extends StatelessWidget {
const ContactItemWidget({required this.contact, super.key});
final proto.Contact contact;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
Widget build(
BuildContext context,
) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../tools/tools.dart';
import '../../theme/theme.dart';
class EmptyContactListWidget extends ConsumerWidget {
class EmptyContactListWidget extends StatelessWidget {
const EmptyContactListWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
Widget build(
BuildContext context,
) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;

View File

@ -0,0 +1,3 @@
export 'contact_item_widget.dart';
export 'contact_list_widget.dart';
export 'empty_contact_list_widget.dart';

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/window_control.dart';
import 'home.dart';
import '../chat/chat.dart';
import '../tools/tools.dart';
class ChatOnlyPage extends StatefulWidget {
const ChatOnlyPage({super.key});
@ -11,7 +10,7 @@ class ChatOnlyPage extends StatefulWidget {
ChatOnlyPageState createState() => ChatOnlyPageState();
}
class ChatOnlyPageState extends ConsumerState<ChatOnlyPage>
class ChatOnlyPageState extends State<ChatOnlyPage>
with TickerProviderStateMixin {
final _unfocusNode = FocusNode();
@ -21,7 +20,7 @@ class ChatOnlyPageState extends ConsumerState<ChatOnlyPage>
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() {});
await ref.read(windowControlProvider.notifier).changeWindowSetup(
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
@ -33,13 +32,9 @@ class ChatOnlyPageState extends ConsumerState<ChatOnlyPage>
}
@override
Widget build(BuildContext context) {
ref.watch(windowControlProvider);
return SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
child: HomePage.buildChatComponent(context, ref),
));
}
Widget build(BuildContext context) => SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
child: buildChatComponent(),
));
}

View File

@ -1,28 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ContactsPage extends ConsumerWidget {
class ContactsPage extends StatelessWidget {
const ContactsPage({super.key});
static const path = '/contacts';
@override
Widget build(BuildContext context, WidgetRef ref) => const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Contacts Page'),
// ElevatedButton(
// onPressed: () async {
// ref.watch(authNotifierProvider.notifier).login(
// "myEmail",
// "myPassword",
// );
// },
// child: const Text("Login"),
// ),
],
Widget build(
BuildContext context,
) =>
const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Contacts Page'),
// ElevatedButton(
// onPressed: () async {
// ref.watch(authNotifierProvider.notifier).login(
// "myEmail",
// "myPassword",
// );
// },
// child: const Text("Login"),
// ),
],
),
),
),
);
);
}

View File

@ -9,7 +9,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../proto/proto.dart' as proto;
import '../account_manager/account_manager.dart';
import '../account_manager/models/models.dart';
import '../chat/chat.dart';
import '../theme/theme.dart';
import '../tools/tools.dart';
import 'main_pager/main_pager.dart';
@ -19,38 +19,6 @@ class HomePage extends StatefulWidget {
@override
HomePageState createState() => HomePageState();
static Widget buildChatComponent() {
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
final activeChat = ref.watch(activeChatStateProvider);
if (activeChat == null) {
return const EmptyChatWidget();
}
final activeAccountInfo =
ref.watch(fetchActiveAccountProvider).asData?.value;
if (activeAccountInfo == null) {
return const EmptyChatWidget();
}
final activeChatContactIdx = contactList.indexWhere(
(c) =>
proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
activeChat,
);
if (activeChatContactIdx == -1) {
ref.read(activeChatStateProvider.notifier).state = null;
return const EmptyChatWidget();
}
final activeChatContact = contactList[activeChatContactIdx];
return ChatComponent(
activeAccountInfo: activeAccountInfo,
activeChat: activeChat,
activeChatContact: activeChatContact);
}
}
class HomePageState extends State<HomePage> with TickerProviderStateMixin {
@ -196,7 +164,7 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
Widget buildTabletLeftPane() =>
Material(color: Colors.transparent, child: buildUserPanel());
Widget buildTabletRightPane() => HomePage.buildChatComponent();
Widget buildTabletRightPane() => buildChatComponent();
// ignore: prefer_expression_function_bodies
Widget buildTablet() => Builder(builder: (context) {

View File

@ -4,20 +4,15 @@ 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:veilid_support/veilid_support.dart';
import '../../../components/contact_invitation_list_widget.dart';
import '../../../components/contact_list_widget.dart';
import '../../../entities/local_account.dart';
import '../../../proto/proto.dart' as proto;
import '../../providers/contact.dart';
import '../../providers/contact_invite.dart';
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import '../../../../packages/veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contact_invitation/contact_invitation.dart';
import '../../contacts/contacts.dart';
class AccountPage extends ConsumerStatefulWidget {
class AccountPage extends StatefulWidget {
const AccountPage({
required this.localAccounts,
required this.activeUserLogin,
@ -41,7 +36,7 @@ class AccountPage extends ConsumerStatefulWidget {
}
}
class AccountPageState extends ConsumerState<AccountPage> {
class AccountPageState extends State<AccountPage> {
final _unfocusNode = FocusNode();
@override

View File

@ -1,8 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class BottomSheetActionButton extends ConsumerStatefulWidget {
class BottomSheetActionButton extends StatefulWidget {
const BottomSheetActionButton(
{required this.bottomSheetBuilder,
required this.builder,
@ -32,8 +31,7 @@ class BottomSheetActionButton extends ConsumerStatefulWidget {
}
}
class BottomSheetActionButtonState
extends ConsumerState<BottomSheetActionButton> {
class BottomSheetActionButtonState extends State<BottomSheetActionButton> {
bool _showFab = true;
@override

View File

@ -1,28 +1,19 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../components/chat_single_contact_list_widget.dart';
import '../../../components/empty_chat_list_widget.dart';
import '../../../entities/local_account.dart';
import '../../../proto/proto.dart' as proto;
import '../../providers/account.dart';
import '../../providers/chat.dart';
import '../../providers/contact.dart';
import '../../../local_accounts/local_accounts.dart';
import '../../providers/logins.dart';
import '../../../tools/tools.dart';
import '../../../../packages/veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../tools/tools.dart';
class ChatsPage extends ConsumerStatefulWidget {
class ChatsPage extends StatefulWidget {
const ChatsPage({super.key});
@override
ChatsPageState createState() => ChatsPageState();
}
class ChatsPageState extends ConsumerState<ChatsPage> {
class ChatsPageState extends State<ChatsPage> {
final _unfocusNode = FocusNode();
@override

View File

@ -1,27 +1,23 @@
import 'dart:async';
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/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:preload_page_view/preload_page_view.dart';
import 'package:stylish_bottom_bar/model/bar_items.dart';
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../components/bottom_sheet_action_button.dart';
import '../../../components/paste_invite_dialog.dart';
import '../../../components/scan_invite_dialog.dart';
import '../../../components/send_invite_dialog.dart';
import '../../../entities/local_account.dart';
import '../../../proto/proto.dart' as proto;
import '../../../tools/tools.dart';
import '../../../../packages/veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contact_invitation/contact_invitation.dart';
import '../../theme/theme.dart';
import 'account.dart';
import 'bottom_sheet_action_button.dart';
import 'chats.dart';
class MainPager extends StatefulWidget {
@ -50,8 +46,7 @@ class MainPager extends StatefulWidget {
}
}
class MainPagerState extends ConsumerState<MainPager>
with TickerProviderStateMixin {
class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
//////////////////////////////////////////////////////////////////
final _unfocusNode = FocusNode();
@ -151,64 +146,6 @@ class MainPagerState extends ConsumerState<MainPager>
});
}
Widget _newContactInvitationBottomSheetBuilder(
// ignore: prefer_expression_function_bodies
BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (ke) {
if (ke.logicalKey == LogicalKeyboardKey.escape) {
Navigator.pop(context);
}
},
child: SizedBox(
height: 200,
child: Column(children: [
Text(translate('accounts_menu.invite_contact'),
style: textTheme.titleMedium)
.paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await SendInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.contact_page),
color: scale.primaryScale.background),
Text(translate('accounts_menu.create_invite'))
]),
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await ScanInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.qr_code_scanner),
color: scale.primaryScale.background),
Text(translate('accounts_menu.scan_invite'))
]),
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await PasteInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.paste),
color: scale.primaryScale.background),
Text(translate('accounts_menu.paste_invite'))
])
]).expanded()
])));
}
// ignore: prefer_expression_function_bodies
Widget _onNewChatBottomSheetBuilder(BuildContext context) {
return const SizedBox(
@ -221,7 +158,7 @@ class MainPagerState extends ConsumerState<MainPager>
Widget _bottomSheetBuilder(BuildContext context) {
if (_currentPage == 0) {
// New contact invitation
return _newContactInvitationBottomSheetBuilder(context);
return newContactInvitationBottomSheetBuilder(context);
} else if (_currentPage == 1) {
// New chat
return _onNewChatBottomSheetBuilder(context);

View File

@ -1,36 +0,0 @@
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,
),
),
],
),
);
}
}

View File

@ -1,35 +0,0 @@
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,
),
),
],
),
);
}
}

View File

@ -1,8 +1,6 @@
import '../tools/tools.dart';
import 'settings.dart';
xxx convert to non-asyncvalue based wrapper since there's always a default here
class PreferencesCubit extends StreamWrapperCubit<Preferences> {
PreferencesCubit(PreferencesRepository repository)
: super(repository.stream, defaultState: repository.value);

View File

@ -1,17 +1,13 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart';
import '../layout/default_app_bar.dart';
import '../theme/theme.dart';
import '../tools/tools.dart';
import '../veilid_processor/veilid_processor.dart';
import 'preferences_cubit.dart';
import 'preferences_repository.dart';
import 'settings.dart';
class SettingsPage extends StatefulWidget {
@ -68,8 +64,8 @@ class SettingsPageState extends State<SettingsPage> {
}
@override
Widget build(BuildContext context) => BlocBuilder<PreferencesCubit,
AsyncValue<Preferences>>(
Widget build(BuildContext context) => AsyncBlocBuilder<PreferencesCubit,
Preferences>(
builder: (context, state) => ThemeSwitchingArea(
child: Scaffold(
// resizeToAvoidBottomInset: false,
@ -94,14 +90,16 @@ class SettingsPageState extends State<SettingsPage> {
label:
Text(translate('settings_page.color_theme'))),
items: _getThemeDropdownItems(),
initialValue: themePreferences.colorPreference,
initialValue: state.themePreferences.colorPreference,
onChanged: (value) async {
final newPrefs = themePreferences.copyWith(
colorPreference: value as ColorPreference);
await themeService.save(newPrefs);
final newPrefs = state.copyWith(
themePreferences: state.themePreferences
.copyWith(
colorPreference:
value as ColorPreference));
switcher.changeTheme(
theme: themeService.get(newPrefs));
ref.invalidate(themeServiceProvider);
theme: newPrefs.themePreferences.themeData());
await PreferencesRepository.instance.set(newPrefs);
setState(() {});
})),
ThemeSwitcher.withTheme(
@ -111,15 +109,17 @@ class SettingsPageState extends State<SettingsPage> {
label: Text(
translate('settings_page.brightness_mode'))),
items: _getBrightnessDropdownItems(),
initialValue: themePreferences.brightnessPreference,
initialValue:
state.themePreferences.brightnessPreference,
onChanged: (value) async {
final newPrefs = themePreferences.copyWith(
brightnessPreference:
value as BrightnessPreference);
await themeService.save(newPrefs);
final newPrefs = state.copyWith(
themePreferences: state.themePreferences
.copyWith(
brightnessPreference:
value as BrightnessPreference));
switcher.changeTheme(
theme: themeService.get(newPrefs));
ref.invalidate(themeServiceProvider);
theme: newPrefs.themePreferences.themeData());
await PreferencesRepository.instance.set(newPrefs);
setState(() {});
})),
],

View File

@ -2,12 +2,11 @@ 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';
import '../theme/theme.dart';
class EnterPasswordDialog extends ConsumerStatefulWidget {
class EnterPasswordDialog extends StatefulWidget {
const EnterPasswordDialog({
this.matchPass,
this.description,
@ -29,7 +28,7 @@ class EnterPasswordDialog extends ConsumerStatefulWidget {
}
}
class EnterPasswordDialogState extends ConsumerState<EnterPasswordDialog> {
class EnterPasswordDialogState extends State<EnterPasswordDialog> {
final passwordController = TextEditingController();
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();

View File

@ -2,13 +2,12 @@ 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';
import '../theme/theme.dart';
class EnterPinDialog extends ConsumerStatefulWidget {
class EnterPinDialog extends StatefulWidget {
const EnterPinDialog({
required this.reenter,
required this.description,
@ -30,7 +29,7 @@ class EnterPinDialog extends ConsumerStatefulWidget {
}
}
class EnterPinDialogState extends ConsumerState<EnterPinDialog> {
class EnterPinDialogState extends State<EnterPinDialog> {
final pinController = TextEditingController();
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();

View File

@ -1,10 +1,13 @@
export 'animations.dart';
export 'enter_password.dart';
export 'enter_pin.dart';
export 'loggy.dart';
export 'phono_byte.dart';
export 'responsive.dart';
export 'scanner_error_widget.dart';
export 'shared_preferences.dart';
export 'state_logger.dart';
export 'stream_listenable.dart';
export 'stream_wrapper_cubit.dart';
export 'widget_helpers.dart';
export 'window_control.dart';

View File

@ -1,6 +1,7 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:motion_toast/motion_toast.dart';
@ -60,6 +61,22 @@ extension AsyncValueBuilderExt<T> on AsyncValue<T> {
asyncValueBuilder<T>(this, builder);
}
class AsyncBlocBuilder<B extends StateStreamable<AsyncValue<S>>, S>
extends BlocBuilder<B, AsyncValue<S>> {
AsyncBlocBuilder({
required BlocWidgetBuilder<S> builder,
Widget Function(BuildContext)? loading,
Widget Function(BuildContext, Object, StackTrace?)? error,
super.key,
super.bloc,
super.buildWhen,
}) : super(
builder: (context, state) => state.when(
loading: () => (loading ?? waitingPage)(context),
error: (e, st) => (error ?? errorPage)(context, e, st),
data: (d) => builder(context, d)));
}
Future<void> showErrorModal(
BuildContext context, String title, String text) async {
await QuickAlert.show(