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": { "scan_invitation_dialog": {
"title": "Scan Contact Invite", "title": "Scan Contact Invite",
"instructions": "Position the contact invite QR code in the frame", "instructions": "Position the contact invite QR code in the frame",
"scan_qr_here": "Click here to scan a contact invite QR code:", "scan_qr_here": "Click here to scan a contact invite QR code with your device's camera:",
"paste_qr_here": "Camera scanning is only available on mobile devices. You can copy a QR code image and paste it here:", "paste_qr_here": "You can copy a QR code image and paste it by clicking here:",
"scan": "Scan", "scan": "Scan",
"paste": "Paste", "paste": "Paste",
"not_an_image": "Pasted data is not an image", "not_an_image": "Pasted data is not an image",
"could_not_decode_image": "Could not decode pasted image", "could_not_decode_image": "Could not decode pasted image",
"not_a_valid_qr_code": "Not a valid QR code", "not_a_valid_qr_code": "Not a valid QR code",
"error": "Failed to capture 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_dialog": {
"enter_pin": "Enter PIN", "enter_pin": "Enter PIN",

View file

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

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_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:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.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); return (signedContactInvitationBytes, contactRequestInboxKey);
} }
@ -222,6 +228,10 @@ class ContactInvitationListCubit
required GetEncryptionKeyCallback getEncryptionKeyCallback, required GetEncryptionKeyCallback getEncryptionKeyCallback,
required CancelRequest cancelRequest, required CancelRequest cancelRequest,
}) async { }) async {
log.debug('validateInvitation:\n'
'bytes=${inviteData.lengthInBytes}\n'
'${hex.encode(inviteData)}');
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
// Get contact request inbox from invitation // 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 'dart:typed_data';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:zxing2/qrcode.dart'; import 'package:zxing2/qrcode.dart';
import '../../notifications/notifications.dart'; import '../../notifications/notifications.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart';
import 'camera_qr_scanner.dart';
import 'invitation_dialog.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 { class ScanInvitationDialog extends StatefulWidget {
const ScanInvitationDialog({required Locator locator, super.key}) const ScanInvitationDialog({required Locator locator, super.key})
: _locator = locator; : _locator = locator;
@ -122,7 +35,7 @@ class ScanInvitationDialog extends StatefulWidget {
} }
class ScanInvitationDialogState extends State<ScanInvitationDialog> { class ScanInvitationDialogState extends State<ScanInvitationDialog> {
bool scanned = false; var _scanned = false;
@override @override
void initState() { void initState() {
@ -131,14 +44,14 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
void onValidationCancelled() { void onValidationCancelled() {
setState(() { setState(() {
scanned = false; _scanned = false;
}); });
} }
void onValidationSuccess() {} void onValidationSuccess() {}
void onValidationFailed() { void onValidationFailed() {
setState(() { setState(() {
scanned = false; _scanned = false;
}); });
} }
@ -146,142 +59,62 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
Future<Uint8List?> scanQRImage(BuildContext context) async { Future<Uint8List?> scanQRImage(BuildContext context) async {
final theme = Theme.of(context); final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!; 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 { try {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => Stack( builder: (context) => Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
MobileScanner( CameraQRScanner(
fit: BoxFit.contain, scanSize: const Size(200, 200),
scanWindow: scanWindow, loadingBuilder: (context) => waitingPage(),
controller: cameraController, errorBuilder: (coRntext, e, st) => errorPage(e, st),
errorBuilder: (context, error) => bottomRowBuilder: (context) => FittedBox(
ScannerErrorWidget(error: error), fit: BoxFit.scaleDown,
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(
child: Text( child: Text(
translate( translate(
'scan_invitation_dialog.instructions'), 'scan_invitation_dialog.instructions'),
overflow: TextOverflow.fade, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: theme.textTheme.labelLarge),
.textTheme
.labelLarge!
.copyWith(color: Colors.white),
), ),
), showNotification: (s) {},
), logError: log.error,
IconButton( cameraErrorMessage:
color: Colors.white, translate('scan_invitation_dialog.camera_error'),
icon: ValueListenableBuilder( deniedErrorMessage:
valueListenable: cameraController, translate('scan_invitation_dialog.permission_error'),
builder: (context, state, child) { deniedWithoutPromptErrorMessage:
switch (state.cameraDirection) { translate('scan_invitation_dialog.permission_error'),
case CameraFacing.front: restrictedErrorMessage:
return const Icon(Icons.camera_front); translate('scan_invitation_dialog.permission_error'),
case CameraFacing.back: onDetect: (result) {
return const Icon(Icons.camera_rear); final byteSegments = result
case CameraFacing.external: .resultMetadata[ResultMetadataType.byteSegments];
return const Icon(Icons.camera_alt); if (byteSegments != null) {
case CameraFacing.unknown: final segs = byteSegments as List<Int8List>;
return const Icon(Icons.question_mark);
final byteData = Uint8List.fromList(segs[0].toList());
return byteData;
} }
return null;
}, },
), onDone: (result) {
iconSize: 32, Navigator.of(context).pop(result);
onPressed: cameraController.switchCamera, }),
),
],
),
),
),
Align( Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: IconButton( child: IconButton(
color: Colors.white, color: Colors.white,
icon: icon: Icon(Icons.close,
Icon(Icons.close, color: scale.grayScale.primary), color: scale.primaryScale.appText),
iconSize: 32, iconSize: 32.scaled(context),
onPressed: () => { onPressed: () {
SchedulerBinding.instance Navigator.of(context).pop();
.addPostFrameCallback((_) {
cameraController.dispose();
Navigator.pop(context);
})
})), })),
], ],
)); ));
} 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 (_) { } on Exception catch (_) {
context context
.read<NotificationsCubit>() .read<NotificationsCubit>()
@ -342,18 +175,16 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
InvitationDialogState dialogState, InvitationDialogState dialogState,
Future<void> Function({required Uint8List inviteData}) Future<void> Function({required Uint8List inviteData})
validateInviteData) { validateInviteData) {
//final theme = Theme.of(context); if (_scanned) {
//final scale = theme.extension<ScaleScheme>()!; return const SizedBox.shrink();
//final textTheme = theme.textTheme; }
//final height = MediaQuery.of(context).size.height;
final children = <Widget>[];
if (isiOS || isAndroid) { if (isiOS || isAndroid) {
return Column(mainAxisSize: MainAxisSize.min, children: [ children.addAll([
if (!scanned)
Text( Text(
translate('scan_invitation_dialog.scan_qr_here'), translate('scan_invitation_dialog.scan_qr_here'),
).paddingLTRB(0, 0, 0, 8), ).paddingLTRB(0, 0, 0, 8),
if (!scanned)
Container( Container(
constraints: const BoxConstraints(maxHeight: 200), constraints: const BoxConstraints(maxHeight: 200),
child: ElevatedButton( child: ElevatedButton(
@ -363,7 +194,7 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
final inviteData = await scanQRImage(context); final inviteData = await scanQRImage(context);
if (inviteData != null) { if (inviteData != null) {
setState(() { setState(() {
scanned = true; _scanned = true;
}); });
await validateInviteData(inviteData: inviteData); await validateInviteData(inviteData: inviteData);
} }
@ -372,12 +203,11 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
).paddingLTRB(0, 0, 0, 8) ).paddingLTRB(0, 0, 0, 8)
]); ]);
} }
return Column(mainAxisSize: MainAxisSize.min, children: [
if (!scanned) children.addAll([
Text( Text(
translate('scan_invitation_dialog.paste_qr_here'), translate('scan_invitation_dialog.paste_qr_here'),
).paddingLTRB(0, 0, 0, 8), ).paddingLTRB(0, 0, 0, 8),
if (!scanned)
Container( Container(
constraints: const BoxConstraints(maxHeight: 200), constraints: const BoxConstraints(maxHeight: 200),
child: ElevatedButton( child: ElevatedButton(
@ -388,19 +218,19 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
if (inviteData != null) { if (inviteData != null) {
await validateInviteData(inviteData: inviteData); await validateInviteData(inviteData: inviteData);
setState(() { setState(() {
scanned = true; _scanned = true;
}); });
} }
}, },
child: Text(translate('scan_invitation_dialog.paste'))), child: Text(translate('scan_invitation_dialog.paste'))),
).paddingLTRB(0, 0, 0, 8) ).paddingLTRB(0, 0, 0, 8)
]); ]);
return Column(mainAxisSize: MainAxisSize.min, children: children);
} }
@override @override
// ignore: prefer_expression_function_bodies Widget build(BuildContext context) => InvitationDialog(
Widget build(BuildContext context) {
return InvitationDialog(
locator: widget._locator, locator: widget._locator,
onValidationCancelled: onValidationCancelled, onValidationCancelled: onValidationCancelled,
onValidationSuccess: onValidationSuccess, onValidationSuccess: onValidationSuccess,
@ -408,10 +238,3 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
inviteControlIsValid: inviteControlIsValid, inviteControlIsValid: inviteControlIsValid,
buildInviteControl: buildInviteControl); 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_display.dart';
export 'contact_invitation_item_widget.dart'; export 'contact_invitation_item_widget.dart';
export 'contact_invitation_list_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 'preferences/preferences.dart';
export 'recovery_key_widget.dart'; export 'recovery_key_widget.dart';
export 'responsive.dart'; export 'responsive.dart';
export 'scanner_error_widget.dart';
export 'styled_widgets/styled_button_box.dart'; export 'styled_widgets/styled_button_box.dart';
export 'styled_widgets/styled_widgets.dart'; export 'styled_widgets/styled_widgets.dart';
export 'widget_helpers.dart'; export 'widget_helpers.dart';

View file

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

View file

@ -2,9 +2,6 @@ PODS:
- file_saver (0.0.1): - file_saver (0.0.1):
- FlutterMacOS - FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- package_info_plus (0.0.1): - package_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- pasteboard (0.0.1): - pasteboard (0.0.1):
@ -34,7 +31,6 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - 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`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - 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 :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin
package_info_plus: package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
pasteboard: pasteboard:
@ -80,7 +74,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: f0052d280d17aa382b932f399edf32507174e870 package_info_plus: f0052d280d17aa382b932f399edf32507174e870
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7 pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 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 /// Will execute the closure multiple times if a consistent write to the DHT
/// is not achieved. Timeout if specified will be thrown as a /// is not achieved. Timeout if specified will be thrown as a
/// TimeoutException. The closure should return a value if its changes also /// 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. /// eventual consistency pass.
Future<T> operateAppendEventual<T>( Future<T> operateAppendEventual<T>(
Future<T> Function(DHTLogWriteOperations) closure, Future<T> Function(DHTLogWriteOperations) closure,

View file

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

View file

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

View file

@ -531,7 +531,7 @@ class _DHTShortArrayHead {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Head/element mutex to ensure we keep the representation valid // 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 // Subscription to head record internal changes
StreamSubscription<DHTRecordWatchChange>? _subscription; StreamSubscription<DHTRecordWatchChange>? _subscription;
// Notify closure for external head changes // Notify closure for external head changes

View file

@ -15,27 +15,37 @@ abstract class DHTAdd {
Future<void> add(Uint8List value); Future<void> add(Uint8List value);
/// Try to add a list of items to the DHT container. /// Try to add a list of items to the DHT container.
/// Return if the elements were successfully added. /// Return the number of elements successfully added.
/// Throws DHTExceptionTryAgain if the state changed before the elements could /// Throws DHTExceptionTryAgain if the state changed before any elements could
/// be added or a newer value was found on the network. /// 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. /// Throws a StateError if the container exceeds its maximum size.
Future<void> addAll(List<Uint8List> values); Future<void> addAll(List<Uint8List> values);
} }
extension DHTAddExt on DHTAdd { extension DHTAddExt on DHTAdd {
/// Convenience function: /// Convenience function:
/// Like tryAddItem but also encodes the input value as JSON and parses the /// Like add but also encodes the input value as JSON
/// returned element as JSON
Future<void> addJson<T>( Future<void> addJson<T>(
T newValue, T newValue,
) => ) =>
add(jsonEncodeBytes(newValue)); add(jsonEncodeBytes(newValue));
/// Convenience function: /// Convenience function:
/// Like tryAddItem but also encodes the input value as a protobuf object /// Like add but also encodes the input value as a protobuf object
/// and parses the returned element as a protobuf object
Future<void> addProtobuf<T extends GeneratedMessage>( Future<void> addProtobuf<T extends GeneratedMessage>(
T newValue, T newValue,
) => ) =>
add(newValue.writeToBuffer()); 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 { class DHTExceptionOutdated implements Exception {
const DHTExceptionOutdated( const DHTExceptionOutdated(
[this.cause = 'operation failed due to newer dht value']); {this.cause = 'operation failed due to newer dht value'});
final String cause; final String cause;
@override @override
String toString() => 'DHTExceptionOutdated: $cause'; 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 { class DHTExceptionInvalidData implements Exception {
const DHTExceptionInvalidData(this.cause); const DHTExceptionInvalidData({this.cause = 'data was invalid'});
final String cause; final String cause;
@override @override
@ -16,7 +27,7 @@ class DHTExceptionInvalidData implements Exception {
} }
class DHTExceptionCancelled implements Exception { class DHTExceptionCancelled implements Exception {
const DHTExceptionCancelled([this.cause = 'operation was cancelled']); const DHTExceptionCancelled({this.cause = 'operation was cancelled'});
final String cause; final String cause;
@override @override
@ -25,7 +36,7 @@ class DHTExceptionCancelled implements Exception {
class DHTExceptionNotAvailable implements Exception { class DHTExceptionNotAvailable implements Exception {
const DHTExceptionNotAvailable( 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; final String cause;
@override @override

View file

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

View file

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