This commit is contained in:
Christien Rioux 2023-09-28 10:06:22 -04:00
parent e5f1619c65
commit 752392c02e
39 changed files with 1025 additions and 435 deletions

View file

@ -75,8 +75,8 @@ class ChatSingleContactItemWidget extends ConsumerWidget {
title: Text(contact.editedProfile.name),
/// xxx show last message here
subtitle: (contact.editedProfile.title.isNotEmpty)
? Text(contact.editedProfile.title)
subtitle: (contact.editedProfile.pronouns.isNotEmpty)
? Text(contact.editedProfile.pronouns)
: null,
iconColor: scale.tertiaryScale.background,
textColor: scale.tertiaryScale.text,

View file

@ -59,7 +59,7 @@ class ChatSingleContactListWidget extends ConsumerWidget {
return contact.editedProfile.name
.toLowerCase()
.contains(lowerValue) ||
contact.editedProfile.title
contact.editedProfile.pronouns
.toLowerCase()
.contains(lowerValue);
}).toList();

View file

@ -105,8 +105,8 @@ class ContactItemWidget extends ConsumerWidget {
// }
},
title: Text(contact.editedProfile.name),
subtitle: (contact.editedProfile.title.isNotEmpty)
? Text(contact.editedProfile.title)
subtitle: (contact.editedProfile.pronouns.isNotEmpty)
? Text(contact.editedProfile.pronouns)
: null,
iconColor: scale.tertiaryScale.background,
textColor: scale.tertiaryScale.text,

View file

@ -45,7 +45,7 @@ class ContactListWidget extends ConsumerWidget {
element.editedProfile.name
.toLowerCase()
.contains(lowerValue) ||
element.editedProfile.title
element.editedProfile.pronouns
.toLowerCase()
.contains(lowerValue))
.toList();

View file

@ -304,14 +304,14 @@ class InviteDialogState extends ConsumerState<InviteDialog> {
if (_validInvitation != null && !_isValidating)
Column(children: [
Container(
constraints: const BoxConstraints(maxHeight: 64),
width: double.infinity,
child: ProfileWidget(
name: _validInvitation!
.contactRequestPrivate.profile.name,
title: _validInvitation!
.contactRequestPrivate.profile.title))
.paddingLTRB(0, 0, 0, 8),
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: [

View file

@ -8,12 +8,12 @@ import '../tools/tools.dart';
class ProfileWidget extends ConsumerWidget {
const ProfileWidget({
required this.name,
this.title,
this.pronouns,
super.key,
});
final String name;
final String? title;
final String? pronouns;
@override
// ignore: prefer_expression_function_bodies
@ -33,8 +33,8 @@ class ProfileWidget extends ConsumerWidget {
style: textTheme.headlineSmall,
textAlign: TextAlign.left,
).paddingAll(4),
if (title != null && title!.isNotEmpty)
Text(title!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4),
if (pronouns != null && pronouns!.isNotEmpty)
Text(pronouns!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4),
]),
);
}
@ -44,6 +44,6 @@ class ProfileWidget extends ConsumerWidget {
super.debugFillProperties(properties);
properties
..add(StringProperty('name', name))
..add(StringProperty('title', title));
..add(StringProperty('pronouns', pronouns));
}
}

View file

@ -1,14 +1,112 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:awesome_extensions/awesome_extensions.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:pasteboard/pasteboard.dart';
import 'package:zxing2/qrcode.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:image/image.dart' as img;
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});
@ -24,53 +122,204 @@ class ScanInviteDialog extends ConsumerStatefulWidget {
}
class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
// final _pasteTextController = TextEditingController();
bool scanned = false;
@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();
// final inviteData = base64UrlNoPadDecode(inviteDataBase64);
// await validateInviteData(inviteData: inviteData);
// }
void onValidationCancelled() {
// _pasteTextController.clear();
setState(() {
scanned = false;
});
}
void onValidationSuccess() {
//_pasteTextController.clear();
}
void onValidationSuccess() {}
void onValidationFailed() {
//_pasteTextController.clear();
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,
@ -78,31 +327,56 @@ class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
validateInviteData) {
final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
//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: [
Text(
translate('scan_invite_dialog.scan_invite_here'),
).paddingLTRB(0, 0, 0, 8),
// Container(
// constraints: const BoxConstraints(maxHeight: 200),
// child: TextField(
// enabled: !dialogState.isValidating,
// onChanged: (text) => _onPasteChanged(text, validateInviteData),
// style: textTheme.labelSmall!
// .copyWith(fontFamily: 'Victor Mono', fontSize: 11),
// 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)
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) {
setState(() {
scanned = true;
});
await validateInviteData(inviteData: inviteData);
}
},
child: Text(translate('scan_invite_dialog.paste'))),
).paddingLTRB(0, 0, 0, 8)
]);
}