mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-07-30 01:38:45 -04:00
new qr code scanner
This commit is contained in:
parent
b192c44d5c
commit
28580bad88
19 changed files with 636 additions and 373 deletions
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
473
lib/contact_invitation/views/camera_qr_scanner.dart
Normal file
473
lib/contact_invitation/views/camera_qr_scanner.dart
Normal 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;
|
||||||
|
}
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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';
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 '
|
||||||
|
|
|
@ -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} ');
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue