new qr code scanner

This commit is contained in:
Christien Rioux 2025-06-05 23:43:13 +02:00
parent b192c44d5c
commit 28580bad88
19 changed files with 636 additions and 373 deletions

View file

@ -225,15 +225,16 @@
"scan_invitation_dialog": {
"title": "Scan Contact Invite",
"instructions": "Position the contact invite QR code in the frame",
"scan_qr_here": "Click here to scan a contact invite QR code:",
"paste_qr_here": "Camera scanning is only available on mobile devices. You can copy a QR code image and paste it here:",
"scan_qr_here": "Click here to scan a contact invite QR code with your device's camera:",
"paste_qr_here": "You can copy a QR code image and paste it by clicking here:",
"scan": "Scan",
"paste": "Paste",
"not_an_image": "Pasted data is not an image",
"could_not_decode_image": "Could not decode pasted image",
"not_a_valid_qr_code": "Not a valid QR code",
"error": "Failed to capture QR code",
"permission_error": "Capturing QR codes requires camera permisions. Allow camera permissions for VeilidChat in your settings."
"permission_error": "Capturing QR codes requires camera permisions. Allow camera permissions for VeilidChat in your settings.",
"camera_error": "Camera error"
},
"enter_pin_dialog": {
"enter_pin": "Enter PIN",

View file

@ -6,9 +6,6 @@ PODS:
- Flutter (1.0.0)
- flutter_native_splash (2.4.3):
- Flutter
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- package_info_plus (0.4.5):
- Flutter
- pasteboard (0.0.1):
@ -38,7 +35,6 @@ DEPENDENCIES:
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@ -59,8 +55,6 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
@ -87,7 +81,6 @@ SPEC CHECKSUMS:
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:convert/convert.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
@ -165,6 +166,11 @@ class ContactInvitationListCubit
});
});
log.debug('createInvitation:\n'
'contactRequestInboxKey=$contactRequestInboxKey\n'
'bytes=${signedContactInvitationBytes.lengthInBytes}\n'
'${hex.encode(signedContactInvitationBytes)}');
return (signedContactInvitationBytes, contactRequestInboxKey);
}
@ -222,6 +228,10 @@ class ContactInvitationListCubit
required GetEncryptionKeyCallback getEncryptionKeyCallback,
required CancelRequest cancelRequest,
}) async {
log.debug('validateInvitation:\n'
'bytes=${inviteData.lengthInBytes}\n'
'${hex.encode(inviteData)}');
final pool = DHTRecordPool.instance;
// Get contact request inbox from invitation

View file

@ -0,0 +1,473 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:zxing2/qrcode.dart';
import '../../theme/theme.dart';
enum _FrameState {
notFound,
formatError,
checksumError,
}
class _ScannerOverlay extends CustomPainter {
_ScannerOverlay(this.scanWindow, this.frameColor);
final Rect scanWindow;
final Color? frameColor;
@override
void paint(Canvas canvas, Size size) {
final backgroundPath = Path()..addRect(Rect.largest);
final cutoutPath = Path()..addRect(scanWindow);
final backgroundPaint = Paint()
..color = (frameColor ?? Colors.black).withAlpha(127)
..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;
}
/// Camera QR scanner
class CameraQRScanner<T> extends StatefulWidget {
const CameraQRScanner(
{required Widget Function(BuildContext) loadingBuilder,
required Widget Function(
BuildContext, Object error, StackTrace? stackTrace)
errorBuilder,
required Widget Function(BuildContext) bottomRowBuilder,
required void Function(String) showNotification,
required void Function(String, Object? error, StackTrace? stackTrace)
logError,
required void Function(T) onDone,
T? Function(Result)? onDetect,
T? Function(CameraImage)? onImageAvailable,
Size? scanSize,
Color? formatErrorFrameColor,
Color? checksumErrorFrameColor,
String? cameraErrorMessage,
String? deniedErrorMessage,
String? deniedWithoutPromptErrorMessage,
String? restrictedErrorMessage,
super.key})
: _loadingBuilder = loadingBuilder,
_errorBuilder = errorBuilder,
_bottomRowBuilder = bottomRowBuilder,
_showNotification = showNotification,
_logError = logError,
_scanSize = scanSize,
_onDetect = onDetect,
_onDone = onDone,
_onImageAvailable = onImageAvailable,
_formatErrorFrameColor = formatErrorFrameColor,
_checksumErrorFrameColor = checksumErrorFrameColor,
_cameraErrorMessage = cameraErrorMessage,
_deniedErrorMessage = deniedErrorMessage,
_deniedWithoutPromptErrorMessage = deniedWithoutPromptErrorMessage,
_restrictedErrorMessage = restrictedErrorMessage;
@override
State<CameraQRScanner<T>> createState() => _CameraQRScannerState();
////////////////////////////////////////////////////////////////////////////
final Widget Function(BuildContext) _loadingBuilder;
final Widget Function(BuildContext, Object error, StackTrace? stackTrace)
_errorBuilder;
final Widget Function(BuildContext) _bottomRowBuilder;
final void Function(String) _showNotification;
final void Function(String, Object? error, StackTrace? stackTrace) _logError;
final T? Function(Result)? _onDetect;
final void Function(T) _onDone;
final T? Function(CameraImage)? _onImageAvailable;
final Size? _scanSize;
final Color? _formatErrorFrameColor;
final Color? _checksumErrorFrameColor;
final String? _cameraErrorMessage;
final String? _deniedErrorMessage;
final String? _deniedWithoutPromptErrorMessage;
final String? _restrictedErrorMessage;
}
class _CameraQRScannerState<T> extends State<CameraQRScanner<T>>
with WidgetsBindingObserver, TickerProviderStateMixin {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Async Init
_initWait.add(_init);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
unawaited(_controller?.dispose());
super.dispose();
}
// #docregion AppLifecycle
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final cameraController = _controller;
// App state changed before we got the chance to initialize.
if (cameraController == null || !cameraController.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
unawaited(cameraController.dispose());
} else if (state == AppLifecycleState.resumed) {
unawaited(_initializeCameraController(cameraController.description));
}
}
// #enddocregion AppLifecycle
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final activeColor = theme.colorScheme.primary;
final inactiveColor = theme.colorScheme.onPrimary;
final scanSize = widget._scanSize;
final scanWindow = scanSize == null
? null
: Rect.fromCenter(
center: Offset.zero,
width: scanSize.width,
height: scanSize.height,
);
return Scaffold(
body: FutureBuilder(
future: _initWait(),
builder: (context, av) => av.when(
error: (e, st) => widget._errorBuilder(context, e, st),
loading: () => widget._loadingBuilder(context),
data: (data, isComplete) => Column(
children: <Widget>[
Expanded(
child: Padding(
padding: const EdgeInsets.all(1),
child: Center(
child: Stack(
alignment: AlignmentDirectional.center,
children: [
_cameraPreviewWidget(context),
if (scanWindow != null)
IgnorePointer(
child: CustomPaint(
foregroundPainter: _ScannerOverlay(
scanWindow,
switch (_frameState) {
_FrameState.notFound => null,
_FrameState.formatError =>
widget._formatErrorFrameColor,
_FrameState.checksumError =>
widget._checksumErrorFrameColor
}),
)),
]),
),
),
),
widget._bottomRowBuilder(context),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_cameraToggleWidget(),
_torchToggleWidget(activeColor, inactiveColor)
],
),
],
),
)));
}
/// Display the preview from the camera
/// (or a message if the preview is not available).
Widget _cameraPreviewWidget(BuildContext context) {
final cameraController = _controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return widget._loadingBuilder(context);
} else {
return Listener(
onPointerDown: (_) => _pointers++,
onPointerUp: (_) => _pointers--,
child: CameraPreview(
cameraController,
child: LayoutBuilder(
builder: (context, constraints) => GestureDetector(
behavior: HitTestBehavior.opaque,
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
onTapDown: (details) =>
_onViewFinderTap(details, constraints),
)),
),
);
}
}
void _handleScaleStart(ScaleStartDetails details) {
_baseScale = _currentScale;
}
Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
// When there are not exactly two fingers on screen don't scale
if (_controller == null || _pointers != 2) {
return;
}
_currentScale = (_baseScale * details.scale)
.clamp(_minAvailableZoom, _maxAvailableZoom);
await _controller!.setZoomLevel(_currentScale);
}
Widget _torchToggleWidget(Color activeColor, Color inactiveColor) =>
IconButton(
icon: const Icon(Icons.highlight),
color: _controller?.value.flashMode == FlashMode.torch
? activeColor
: inactiveColor,
onPressed: _controller != null
? () => _onSetFlashModeButtonPressed(
_controller?.value.flashMode == FlashMode.torch
? FlashMode.off
: FlashMode.torch)
: null,
);
Widget _cameraToggleWidget() {
final currentCameraDescription = _controller?.description;
return IconButton(
icon:
Icon(isAndroid ? Icons.flip_camera_android : Icons.flip_camera_ios),
onPressed: (currentCameraDescription == null || _cameras.isEmpty)
? null
: () {
final nextCameraIndex =
(_cameras.indexOf(currentCameraDescription) + 1) %
_cameras.length;
unawaited(_onNewCameraSelected(_cameras[nextCameraIndex]));
});
}
void _onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
final cameraController = _controller;
if (cameraController == null) {
return;
}
final offset = Offset(
details.localPosition.dx / constraints.maxWidth,
details.localPosition.dy / constraints.maxHeight,
);
unawaited(cameraController.setExposurePoint(offset));
unawaited(cameraController.setFocusPoint(offset));
}
Future<void> _onNewCameraSelected(CameraDescription cameraDescription) {
if (_controller != null) {
return _controller!.setDescription(cameraDescription);
} else {
return _initializeCameraController(cameraDescription);
}
}
Future<void> _initializeCameraController(
CameraDescription cameraDescription) async {
final cameraController = CameraController(
cameraDescription,
kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.jpeg,
);
_controller = cameraController;
// If the controller is updated then update the UI.
cameraController.addListener(() {
if (mounted) {
setState(() {});
}
if (cameraController.value.hasError &&
(cameraController.value.errorDescription?.isNotEmpty ?? false)) {
widget._showNotification(
'${widget._cameraErrorMessage ?? 'Camera error'}: '
'${cameraController.value.errorDescription!}');
}
});
try {
await cameraController.initialize();
try {
_maxAvailableZoom = await cameraController.getMaxZoomLevel();
_minAvailableZoom = await cameraController.getMinZoomLevel();
} on PlatformException {
_maxAvailableZoom = 1;
_minAvailableZoom = 1;
}
await cameraController.startImageStream((cameraImage) {
final out =
(widget._onImageAvailable ?? _onImageAvailable)(cameraImage);
if (out != null) {
_controller = null;
unawaited(cameraController.dispose());
widget._onDone(out);
}
});
} on CameraException catch (e, st) {
switch (e.code) {
case 'CameraAccessDenied':
widget._showNotification(
widget._deniedErrorMessage ?? 'You have denied camera access.');
case 'CameraAccessDeniedWithoutPrompt':
// iOS only
widget._showNotification(widget._deniedWithoutPromptErrorMessage ??
'Please go to Settings app to enable camera access.');
case 'CameraAccessRestricted':
// iOS only
widget._showNotification(
widget._restrictedErrorMessage ?? 'Camera access is restricted.');
default:
_showCameraException(e, st);
}
}
if (mounted) {
setState(() {});
}
}
T? _onImageAvailable(CameraImage cameraImage) {
try {
final plane = cameraImage.planes.firstOrNull;
if (plane == null) {
return null;
}
// final image = JpegDecoder().decode(plane.bytes);
// if (image == null) {
// return;
// }
// final abgrImage = image
// .convert(numChannels: 4)
// .getBytes(order: ChannelOrder.abgr)
// .buffer
// .asInt32List();
final abgrImage = plane.bytes.buffer.asInt32List();
final source =
RGBLuminanceSource(cameraImage.width, cameraImage.height, abgrImage);
final bitmap = BinaryBitmap(HybridBinarizer(source));
final reader = QRCodeReader();
try {
final result = reader.decode(bitmap);
return widget._onDetect?.call(result);
} on NotFoundException {
_setFrameState(_FrameState.notFound);
} on FormatReaderException {
_setFrameState(_FrameState.formatError);
} on ChecksumException {
_setFrameState(_FrameState.checksumError);
}
// Should also catch errors from QRCodeReader
// ignore: avoid_catches_without_on_clauses
} catch (e, st) {
widget._logError('Unexpected error: $e\n$st', e, st);
}
return null;
}
void _setFrameState(_FrameState frameState) {
if (mounted) {
if (_frameState != frameState) {
setState(() {
_frameState = frameState;
});
}
}
}
void _onSetFlashModeButtonPressed(FlashMode mode) {
unawaited(_setFlashMode(mode).then((_) {
if (mounted) {
setState(() {});
}
}));
}
Future<void> _setFlashMode(FlashMode mode) async {
if (_controller == null) {
return;
}
try {
await _controller!.setFlashMode(mode);
} on CameraException catch (e, st) {
_showCameraException(e, st);
rethrow;
}
}
void _showCameraException(CameraException e, StackTrace st) {
_logCameraException(e, st);
widget._showNotification('Error: ${e.code}\n${e.description}');
}
void _logCameraException(CameraException e, StackTrace st) {
final code = e.code;
final message = e.description;
widget._logError(
'CameraException: $code${message == null ? '' : '\nMessage: $message'}',
e,
st);
}
Future<void> _init(Completer<void> cancel) async {
_cameras = await availableCameras();
if (_cameras.isNotEmpty) {
await _onNewCameraSelected(_cameras.first);
}
}
////////////////////////////////////////////////////////////////////////////
CameraController? _controller;
final _initWait = WaitSet<void, void>();
late final List<CameraDescription> _cameras;
var _minAvailableZoom = 1.0;
var _maxAvailableZoom = 1.0;
var _currentScale = 1.0;
var _baseScale = 1.0;
var _pointers = 0;
_FrameState _frameState = _FrameState.notFound;
}

View file

@ -2,106 +2,19 @@ import 'dart:async';
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:provider/provider.dart';
import 'package:zxing2/qrcode.dart';
import '../../notifications/notifications.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import 'camera_qr_scanner.dart';
import 'invitation_dialog.dart';
// class BarcodeOverlay extends CustomPainter {
// BarcodeOverlay({
// required this.barcode,
// required this.boxFit,
// required this.capture,
// required this.size,
// });
// final BarcodeCapture capture;
// final Barcode barcode;
// final BoxFit boxFit;
// final Size size;
// @override
// void paint(Canvas canvas, Size size) {
// final adjustedSize = applyBoxFit(boxFit, 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.size.width : size.width) /
// adjustedSize.destination.width;
// final ratioHeight = (Platform.isIOS ? capture.size.height : 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.withAlpha(127)
..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 ScanInvitationDialog extends StatefulWidget {
const ScanInvitationDialog({required Locator locator, super.key})
: _locator = locator;
@ -122,7 +35,7 @@ class ScanInvitationDialog extends StatefulWidget {
}
class ScanInvitationDialogState extends State<ScanInvitationDialog> {
bool scanned = false;
var _scanned = false;
@override
void initState() {
@ -131,14 +44,14 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
void onValidationCancelled() {
setState(() {
scanned = false;
_scanned = false;
});
}
void onValidationSuccess() {}
void onValidationFailed() {
setState(() {
scanned = false;
_scanned = false;
});
}
@ -146,142 +59,62 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
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) =>
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.withAlpha(127),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: cameraController,
builder: (context, state, child) {
switch (state.torchState) {
case TorchState.off:
return Icon(Icons.flash_off,
color:
scale.grayScale.subtleBackground);
case TorchState.on:
return Icon(Icons.flash_on,
color: scale.primaryScale.primary);
case TorchState.auto:
return Icon(Icons.flash_auto,
color: scale.primaryScale.primary);
case TorchState.unavailable:
return Icon(Icons.no_flash,
color: scale.primaryScale.primary);
}
},
),
iconSize: 32,
onPressed: cameraController.toggleTorch,
),
SizedBox(
width: windowSize.width - 120,
height: 50,
child: FittedBox(
CameraQRScanner(
scanSize: const Size(200, 200),
loadingBuilder: (context) => waitingPage(),
errorBuilder: (coRntext, e, st) => errorPage(e, st),
bottomRowBuilder: (context) => FittedBox(
fit: BoxFit.scaleDown,
child: Text(
translate(
'scan_invitation_dialog.instructions'),
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.labelLarge!
.copyWith(color: Colors.white),
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelLarge),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: cameraController,
builder: (context, state, child) {
switch (state.cameraDirection) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
case CameraFacing.external:
return const Icon(Icons.camera_alt);
case CameraFacing.unknown:
return const Icon(Icons.question_mark);
showNotification: (s) {},
logError: log.error,
cameraErrorMessage:
translate('scan_invitation_dialog.camera_error'),
deniedErrorMessage:
translate('scan_invitation_dialog.permission_error'),
deniedWithoutPromptErrorMessage:
translate('scan_invitation_dialog.permission_error'),
restrictedErrorMessage:
translate('scan_invitation_dialog.permission_error'),
onDetect: (result) {
final byteSegments = result
.resultMetadata[ResultMetadataType.byteSegments];
if (byteSegments != null) {
final segs = byteSegments as List<Int8List>;
final byteData = Uint8List.fromList(segs[0].toList());
return byteData;
}
return null;
},
),
iconSize: 32,
onPressed: cameraController.switchCamera,
),
],
),
),
),
onDone: (result) {
Navigator.of(context).pop(result);
}),
Align(
alignment: Alignment.topRight,
child: IconButton(
color: Colors.white,
icon:
Icon(Icons.close, color: scale.grayScale.primary),
iconSize: 32,
onPressed: () => {
SchedulerBinding.instance
.addPostFrameCallback((_) {
cameraController.dispose();
Navigator.pop(context);
})
icon: Icon(Icons.close,
color: scale.primaryScale.appText),
iconSize: 32.scaled(context),
onPressed: () {
Navigator.of(context).pop();
})),
],
));
} on MobileScannerException catch (e) {
if (e.errorCode == MobileScannerErrorCode.permissionDenied) {
context
.read<NotificationsCubit>()
.error(text: translate('scan_invitation_dialog.permission_error'));
} else {
context
.read<NotificationsCubit>()
.error(text: translate('scan_invitation_dialog.error'));
}
} on Exception catch (_) {
context
.read<NotificationsCubit>()
@ -342,18 +175,16 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
InvitationDialogState 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 (_scanned) {
return const SizedBox.shrink();
}
final children = <Widget>[];
if (isiOS || isAndroid) {
return Column(mainAxisSize: MainAxisSize.min, children: [
if (!scanned)
children.addAll([
Text(
translate('scan_invitation_dialog.scan_qr_here'),
).paddingLTRB(0, 0, 0, 8),
if (!scanned)
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: ElevatedButton(
@ -363,7 +194,7 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
final inviteData = await scanQRImage(context);
if (inviteData != null) {
setState(() {
scanned = true;
_scanned = true;
});
await validateInviteData(inviteData: inviteData);
}
@ -372,12 +203,11 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
).paddingLTRB(0, 0, 0, 8)
]);
}
return Column(mainAxisSize: MainAxisSize.min, children: [
if (!scanned)
children.addAll([
Text(
translate('scan_invitation_dialog.paste_qr_here'),
).paddingLTRB(0, 0, 0, 8),
if (!scanned)
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: ElevatedButton(
@ -388,19 +218,19 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
if (inviteData != null) {
await validateInviteData(inviteData: inviteData);
setState(() {
scanned = true;
_scanned = true;
});
}
},
child: Text(translate('scan_invitation_dialog.paste'))),
).paddingLTRB(0, 0, 0, 8)
]);
return Column(mainAxisSize: MainAxisSize.min, children: children);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return InvitationDialog(
Widget build(BuildContext context) => InvitationDialog(
locator: widget._locator,
onValidationCancelled: onValidationCancelled,
onValidationSuccess: onValidationSuccess,
@ -408,10 +238,3 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
inviteControlIsValid: inviteControlIsValid,
buildInviteControl: buildInviteControl);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('scanned', scanned));
}
}

View file

@ -1,3 +1,4 @@
export 'camera_qr_scanner.dart';
export 'contact_invitation_display.dart';
export 'contact_invitation_item_widget.dart';
export 'contact_invitation_list_widget.dart';

View file

@ -1,54 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScannerErrorWidget extends StatelessWidget {
const ScannerErrorWidget({required this.error, super.key});
final MobileScannerException error;
@override
Widget build(BuildContext context) {
String errorMessage;
switch (error.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
errorMessage = 'Controller not ready.';
case MobileScannerErrorCode.permissionDenied:
errorMessage = 'Permission denied';
case MobileScannerErrorCode.unsupported:
errorMessage = 'Scanning is unsupported on this device';
default:
errorMessage = 'Generic Error';
}
return ColoredBox(
color: Colors.black,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 16),
child: Icon(Icons.error, color: Colors.white),
),
Text(
errorMessage,
style: const TextStyle(color: Colors.white),
),
Text(
error.errorDetails?.message ?? '',
style: const TextStyle(color: Colors.white),
),
],
),
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<MobileScannerException>('error', error));
}
}

View file

@ -4,7 +4,6 @@ export 'pop_control.dart';
export 'preferences/preferences.dart';
export 'recovery_key_widget.dart';
export 'responsive.dart';
export 'scanner_error_widget.dart';
export 'styled_widgets/styled_button_box.dart';
export 'styled_widgets/styled_widgets.dart';
export 'widget_helpers.dart';

View file

@ -6,7 +6,6 @@ import FlutterMacOS
import Foundation
import file_saver
import mobile_scanner
import package_info_plus
import pasteboard
import path_provider_foundation
@ -21,7 +20,6 @@ import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View file

@ -2,9 +2,6 @@ PODS:
- file_saver (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- pasteboard (0.0.1):
@ -34,7 +31,6 @@ PODS:
DEPENDENCIES:
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
@ -52,8 +48,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
FlutterMacOS:
:path: Flutter/ephemeral
mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
pasteboard:
@ -80,7 +74,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564

View file

@ -243,7 +243,7 @@ class DHTLog implements DHTDeleteable<DHTLog> {
/// Will execute the closure multiple times if a consistent write to the DHT
/// is not achieved. Timeout if specified will be thrown as a
/// TimeoutException. The closure should return a value if its changes also
/// succeeded, and throw DHTExceptionTryAgain to trigger another
/// succeeded, and throw DHTExceptionOutdated to trigger another
/// eventual consistency pass.
Future<T> operateAppendEventual<T>(
Future<T> Function(DHTLogWriteOperations) closure,

View file

@ -250,7 +250,8 @@ class _DHTLogSpine {
final headDelta = _ringDistance(newHead, oldHead);
final tailDelta = _ringDistance(newTail, oldTail);
if (headDelta > _positionLimit ~/ 2 || tailDelta > _positionLimit ~/ 2) {
throw DHTExceptionInvalidData('_DHTLogSpine::_updateHead '
throw DHTExceptionInvalidData(
cause: '_DHTLogSpine::_updateHead '
'_head=$_head _tail=$_tail '
'oldHead=$oldHead oldTail=$oldTail '
'newHead=$newHead newTail=$newTail '

View file

@ -18,7 +18,8 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
final lookup = await _spine.lookupPosition(pos);
if (lookup == null) {
throw DHTExceptionInvalidData(
'_DHTLogRead::tryWriteItem pos=$pos _spine.length=${_spine.length}');
cause: '_DHTLogRead::tryWriteItem pos=$pos '
'_spine.length=${_spine.length}');
}
// Write item to the segment
@ -75,7 +76,8 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
final lookup = await _spine.lookupPosition(insertPos + valueIdx);
if (lookup == null) {
throw DHTExceptionInvalidData('_DHTLogWrite::addAll '
throw DHTExceptionInvalidData(
cause: '_DHTLogWrite::addAll '
'_spine.length=${_spine.length}'
'insertPos=$insertPos valueIdx=$valueIdx '
'values.length=${values.length} ');

View file

@ -531,7 +531,7 @@ class _DHTShortArrayHead {
////////////////////////////////////////////////////////////////////////////
// Head/element mutex to ensure we keep the representation valid
final Mutex _headMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null);
final _headMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null);
// Subscription to head record internal changes
StreamSubscription<DHTRecordWatchChange>? _subscription;
// Notify closure for external head changes

View file

@ -15,27 +15,37 @@ abstract class DHTAdd {
Future<void> add(Uint8List value);
/// Try to add a list of items to the DHT container.
/// Return if the elements were successfully added.
/// Throws DHTExceptionTryAgain if the state changed before the elements could
/// Return the number of elements successfully added.
/// Throws DHTExceptionTryAgain if the state changed before any elements could
/// be added or a newer value was found on the network.
/// Throws DHTConcurrencyLimit if the number values in the list was too large
/// at this time
/// Throws a StateError if the container exceeds its maximum size.
Future<void> addAll(List<Uint8List> values);
}
extension DHTAddExt on DHTAdd {
/// Convenience function:
/// Like tryAddItem but also encodes the input value as JSON and parses the
/// returned element as JSON
/// Like add but also encodes the input value as JSON
Future<void> addJson<T>(
T newValue,
) =>
add(jsonEncodeBytes(newValue));
/// Convenience function:
/// Like tryAddItem but also encodes the input value as a protobuf object
/// and parses the returned element as a protobuf object
/// Like add but also encodes the input value as a protobuf object
Future<void> addProtobuf<T extends GeneratedMessage>(
T newValue,
) =>
add(newValue.writeToBuffer());
/// Convenience function:
/// Like addAll but also encodes the input values as JSON
Future<void> addAllJson<T>(List<T> values) =>
addAll(values.map(jsonEncodeBytes).toList());
/// Convenience function:
/// Like addAll but also encodes the input values as protobuf objects
Future<void> addAllProtobuf<T extends GeneratedMessage>(List<T> values) =>
addAll(values.map((x) => x.writeToBuffer()).toList());
}

View file

@ -0,0 +1,9 @@
////////////////////////////////////////////////////////////////////////////
// Writer interface
// ignore: one_member_abstracts
abstract class DHTRandomSwap {
/// Swap items at position 'aPos' and 'bPos' in the DHTArray.
/// Throws an IndexError if either of the positions swapped exceeds the length
/// of the container
Future<void> swap(int aPos, int bPos);
}

View file

@ -1,14 +1,25 @@
class DHTExceptionOutdated implements Exception {
const DHTExceptionOutdated(
[this.cause = 'operation failed due to newer dht value']);
{this.cause = 'operation failed due to newer dht value'});
final String cause;
@override
String toString() => 'DHTExceptionOutdated: $cause';
}
class DHTConcurrencyLimit implements Exception {
const DHTConcurrencyLimit(
{required this.limit,
this.cause = 'failed due to maximum parallel operation limit'});
final String cause;
final int limit;
@override
String toString() => 'DHTConcurrencyLimit: $cause (limit=$limit)';
}
class DHTExceptionInvalidData implements Exception {
const DHTExceptionInvalidData(this.cause);
const DHTExceptionInvalidData({this.cause = 'data was invalid'});
final String cause;
@override
@ -16,7 +27,7 @@ class DHTExceptionInvalidData implements Exception {
}
class DHTExceptionCancelled implements Exception {
const DHTExceptionCancelled([this.cause = 'operation was cancelled']);
const DHTExceptionCancelled({this.cause = 'operation was cancelled'});
final String cause;
@override
@ -25,7 +36,7 @@ class DHTExceptionCancelled implements Exception {
class DHTExceptionNotAvailable implements Exception {
const DHTExceptionNotAvailable(
[this.cause = 'request could not be completed at this time']);
{this.cause = 'request could not be completed at this time'});
final String cause;
@override

View file

@ -274,7 +274,7 @@ packages:
source: hosted
version: "1.3.1"
camera:
dependency: transitive
dependency: "direct main"
description:
name: camera
sha256: "413d2b34fe28496c35c69ede5b232fb9dd5ca2c3a4cb606b14efc1c7546cc8cb"
@ -394,7 +394,7 @@ packages:
source: hosted
version: "1.19.1"
convert:
dependency: transitive
dependency: "direct main"
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
@ -912,14 +912,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "72f06a071aa8b14acea3ab43ea7949eefe4a2469731ae210e006ba330a033a8c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
nested:
dependency: transitive
description:
@ -1765,7 +1757,7 @@ packages:
path: "../veilid/veilid-flutter"
relative: true
source: path
version: "0.4.6"
version: "0.4.7"
veilid_support:
dependency: "direct main"
description:

View file

@ -23,10 +23,12 @@ dependencies:
bloc: ^9.0.0
bloc_advanced_tools: ^0.1.13
blurry_modal_progress_hud: ^1.1.1
camera: ^0.11.1
change_case: ^2.2.0
charcode: ^1.4.0
circular_profile_avatar: ^2.0.5
circular_reveal_animation: ^2.0.1
convert: ^3.1.2
cupertino_icons: ^1.0.8
equatable: ^2.0.7
expansion_tile_group: ^2.2.0
@ -58,7 +60,6 @@ dependencies:
json_annotation: ^4.9.0
loggy: ^2.0.3
meta: ^1.16.0
mobile_scanner: ^7.0.0
package_info_plus: ^8.3.0
pasteboard: ^0.4.0
path: ^1.9.1