mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-07-29 01:08:36 -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": {
|
||||
"title": "Scan Contact Invite",
|
||||
"instructions": "Position the contact invite QR code in the frame",
|
||||
"scan_qr_here": "Click here to scan a contact invite QR code:",
|
||||
"paste_qr_here": "Camera scanning is only available on mobile devices. You can copy a QR code image and paste it here:",
|
||||
"scan_qr_here": "Click here to scan a contact invite QR code with your device's camera:",
|
||||
"paste_qr_here": "You can copy a QR code image and paste it by clicking here:",
|
||||
"scan": "Scan",
|
||||
"paste": "Paste",
|
||||
"not_an_image": "Pasted data is not an image",
|
||||
"could_not_decode_image": "Could not decode pasted image",
|
||||
"not_a_valid_qr_code": "Not a valid QR code",
|
||||
"error": "Failed to capture QR code",
|
||||
"permission_error": "Capturing QR codes requires camera permisions. Allow camera permissions for VeilidChat in your settings."
|
||||
"permission_error": "Capturing QR codes requires camera permisions. Allow camera permissions for VeilidChat in your settings.",
|
||||
"camera_error": "Camera error"
|
||||
},
|
||||
"enter_pin_dialog": {
|
||||
"enter_pin": "Enter PIN",
|
||||
|
|
|
@ -6,9 +6,6 @@ PODS:
|
|||
- Flutter (1.0.0)
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- pasteboard (0.0.1):
|
||||
|
@ -38,7 +35,6 @@ DEPENDENCIES:
|
|||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
|
@ -59,8 +55,6 @@ EXTERNAL SOURCES:
|
|||
:path: Flutter
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
mobile_scanner:
|
||||
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
pasteboard:
|
||||
|
@ -87,7 +81,6 @@ SPEC CHECKSUMS:
|
|||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -165,6 +166,11 @@ class ContactInvitationListCubit
|
|||
});
|
||||
});
|
||||
|
||||
log.debug('createInvitation:\n'
|
||||
'contactRequestInboxKey=$contactRequestInboxKey\n'
|
||||
'bytes=${signedContactInvitationBytes.lengthInBytes}\n'
|
||||
'${hex.encode(signedContactInvitationBytes)}');
|
||||
|
||||
return (signedContactInvitationBytes, contactRequestInboxKey);
|
||||
}
|
||||
|
||||
|
@ -222,6 +228,10 @@ class ContactInvitationListCubit
|
|||
required GetEncryptionKeyCallback getEncryptionKeyCallback,
|
||||
required CancelRequest cancelRequest,
|
||||
}) async {
|
||||
log.debug('validateInvitation:\n'
|
||||
'bytes=${inviteData.lengthInBytes}\n'
|
||||
'${hex.encode(inviteData)}');
|
||||
|
||||
final pool = DHTRecordPool.instance;
|
||||
|
||||
// Get contact request inbox from invitation
|
||||
|
|
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 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import 'camera_qr_scanner.dart';
|
||||
import 'invitation_dialog.dart';
|
||||
|
||||
// class BarcodeOverlay extends CustomPainter {
|
||||
// BarcodeOverlay({
|
||||
// required this.barcode,
|
||||
// required this.boxFit,
|
||||
// required this.capture,
|
||||
// required this.size,
|
||||
// });
|
||||
|
||||
// final BarcodeCapture capture;
|
||||
// final Barcode barcode;
|
||||
// final BoxFit boxFit;
|
||||
// final Size size;
|
||||
|
||||
// @override
|
||||
// void paint(Canvas canvas, Size size) {
|
||||
// final adjustedSize = applyBoxFit(boxFit, size, size);
|
||||
|
||||
// var verticalPadding = size.height - adjustedSize.destination.height;
|
||||
// var horizontalPadding = size.width - adjustedSize.destination.width;
|
||||
// if (verticalPadding > 0) {
|
||||
// verticalPadding = verticalPadding / 2;
|
||||
// } else {
|
||||
// verticalPadding = 0;
|
||||
// }
|
||||
|
||||
// if (horizontalPadding > 0) {
|
||||
// horizontalPadding = horizontalPadding / 2;
|
||||
// } else {
|
||||
// horizontalPadding = 0;
|
||||
// }
|
||||
|
||||
// final ratioWidth = (Platform.isIOS ? capture.size.width : size.width) /
|
||||
// adjustedSize.destination.width;
|
||||
// final ratioHeight = (Platform.isIOS ? capture.size.height : size.height) /
|
||||
// adjustedSize.destination.height;
|
||||
|
||||
// final adjustedOffset = <Offset>[];
|
||||
// for (final offset in barcode.corners) {
|
||||
// adjustedOffset.add(
|
||||
// Offset(
|
||||
// offset.dx / ratioWidth + horizontalPadding,
|
||||
// offset.dy / ratioHeight + verticalPadding,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// final cutoutPath = Path()..addPolygon(adjustedOffset, true);
|
||||
|
||||
// final backgroundPaint = Paint()
|
||||
// ..color = Colors.red.withOpacity(0.3)
|
||||
// ..style = PaintingStyle.fill
|
||||
// ..blendMode = BlendMode.dstOut;
|
||||
|
||||
// canvas.drawPath(cutoutPath, backgroundPaint);
|
||||
// }
|
||||
|
||||
// @override
|
||||
// bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
// }
|
||||
|
||||
class ScannerOverlay extends CustomPainter {
|
||||
ScannerOverlay(this.scanWindow);
|
||||
|
||||
final Rect scanWindow;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final backgroundPath = Path()..addRect(Rect.largest);
|
||||
final cutoutPath = Path()..addRect(scanWindow);
|
||||
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.black.withAlpha(127)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.dstOut;
|
||||
|
||||
final backgroundWithCutout = Path.combine(
|
||||
PathOperation.difference,
|
||||
backgroundPath,
|
||||
cutoutPath,
|
||||
);
|
||||
canvas.drawPath(backgroundWithCutout, backgroundPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class ScanInvitationDialog extends StatefulWidget {
|
||||
const ScanInvitationDialog({required Locator locator, super.key})
|
||||
: _locator = locator;
|
||||
|
@ -122,7 +35,7 @@ class ScanInvitationDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
||||
bool scanned = false;
|
||||
var _scanned = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -131,14 +44,14 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
|||
|
||||
void onValidationCancelled() {
|
||||
setState(() {
|
||||
scanned = false;
|
||||
_scanned = false;
|
||||
});
|
||||
}
|
||||
|
||||
void onValidationSuccess() {}
|
||||
void onValidationFailed() {
|
||||
setState(() {
|
||||
scanned = false;
|
||||
_scanned = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -146,142 +59,62 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
|||
|
||||
Future<Uint8List?> scanQRImage(BuildContext context) async {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
//final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0);
|
||||
//final maxDialogHeight = windowSize.height - 64.0;
|
||||
|
||||
final scanWindow = Rect.fromCenter(
|
||||
center: MediaQuery.of(context).size.center(Offset.zero),
|
||||
width: 200,
|
||||
height: 200,
|
||||
);
|
||||
|
||||
final cameraController = MobileScannerController();
|
||||
try {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
MobileScanner(
|
||||
fit: BoxFit.contain,
|
||||
scanWindow: scanWindow,
|
||||
controller: cameraController,
|
||||
errorBuilder: (context, error) =>
|
||||
ScannerErrorWidget(error: error),
|
||||
onDetect: (c) {
|
||||
final barcode = c.barcodes.firstOrNull;
|
||||
|
||||
final barcodeBytes = barcode?.rawBytes;
|
||||
if (barcodeBytes != null) {
|
||||
cameraController.dispose();
|
||||
Navigator.pop(context, barcodeBytes);
|
||||
}
|
||||
}),
|
||||
CustomPaint(
|
||||
painter: ScannerOverlay(scanWindow),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
height: 100,
|
||||
color: Colors.black.withAlpha(127),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: ValueListenableBuilder(
|
||||
valueListenable: cameraController,
|
||||
builder: (context, state, child) {
|
||||
switch (state.torchState) {
|
||||
case TorchState.off:
|
||||
return Icon(Icons.flash_off,
|
||||
color:
|
||||
scale.grayScale.subtleBackground);
|
||||
case TorchState.on:
|
||||
return Icon(Icons.flash_on,
|
||||
color: scale.primaryScale.primary);
|
||||
case TorchState.auto:
|
||||
return Icon(Icons.flash_auto,
|
||||
color: scale.primaryScale.primary);
|
||||
case TorchState.unavailable:
|
||||
return Icon(Icons.no_flash,
|
||||
color: scale.primaryScale.primary);
|
||||
}
|
||||
},
|
||||
),
|
||||
iconSize: 32,
|
||||
onPressed: cameraController.toggleTorch,
|
||||
),
|
||||
SizedBox(
|
||||
width: windowSize.width - 120,
|
||||
height: 50,
|
||||
child: FittedBox(
|
||||
child: Text(
|
||||
CameraQRScanner(
|
||||
scanSize: const Size(200, 200),
|
||||
loadingBuilder: (context) => waitingPage(),
|
||||
errorBuilder: (coRntext, e, st) => errorPage(e, st),
|
||||
bottomRowBuilder: (context) => FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
translate(
|
||||
'scan_invitation_dialog.instructions'),
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelLarge),
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: ValueListenableBuilder(
|
||||
valueListenable: cameraController,
|
||||
builder: (context, state, child) {
|
||||
switch (state.cameraDirection) {
|
||||
case CameraFacing.front:
|
||||
return const Icon(Icons.camera_front);
|
||||
case CameraFacing.back:
|
||||
return const Icon(Icons.camera_rear);
|
||||
case CameraFacing.external:
|
||||
return const Icon(Icons.camera_alt);
|
||||
case CameraFacing.unknown:
|
||||
return const Icon(Icons.question_mark);
|
||||
}
|
||||
},
|
||||
),
|
||||
iconSize: 32,
|
||||
onPressed: cameraController.switchCamera,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
showNotification: (s) {},
|
||||
logError: log.error,
|
||||
cameraErrorMessage:
|
||||
translate('scan_invitation_dialog.camera_error'),
|
||||
deniedErrorMessage:
|
||||
translate('scan_invitation_dialog.permission_error'),
|
||||
deniedWithoutPromptErrorMessage:
|
||||
translate('scan_invitation_dialog.permission_error'),
|
||||
restrictedErrorMessage:
|
||||
translate('scan_invitation_dialog.permission_error'),
|
||||
onDetect: (result) {
|
||||
final byteSegments = result
|
||||
.resultMetadata[ResultMetadataType.byteSegments];
|
||||
if (byteSegments != null) {
|
||||
final segs = byteSegments as List<Int8List>;
|
||||
|
||||
final byteData = Uint8List.fromList(segs[0].toList());
|
||||
return byteData;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onDone: (result) {
|
||||
Navigator.of(context).pop(result);
|
||||
}),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
color: Colors.white,
|
||||
icon:
|
||||
Icon(Icons.close, color: scale.grayScale.primary),
|
||||
iconSize: 32,
|
||||
onPressed: () => {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((_) {
|
||||
cameraController.dispose();
|
||||
Navigator.pop(context);
|
||||
})
|
||||
})),
|
||||
icon: Icon(Icons.close,
|
||||
color: scale.primaryScale.appText),
|
||||
iconSize: 32.scaled(context),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
})),
|
||||
],
|
||||
));
|
||||
} on MobileScannerException catch (e) {
|
||||
if (e.errorCode == MobileScannerErrorCode.permissionDenied) {
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.error(text: translate('scan_invitation_dialog.permission_error'));
|
||||
} else {
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.error(text: translate('scan_invitation_dialog.error'));
|
||||
}
|
||||
} on Exception catch (_) {
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
|
@ -342,76 +175,66 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
|||
InvitationDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData) {
|
||||
//final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
//final textTheme = theme.textTheme;
|
||||
//final height = MediaQuery.of(context).size.height;
|
||||
|
||||
if (isiOS || isAndroid) {
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (!scanned)
|
||||
Text(
|
||||
translate('scan_invitation_dialog.scan_qr_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
if (!scanned)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ElevatedButton(
|
||||
onPressed: dialogState.isValidating
|
||||
? null
|
||||
: () async {
|
||||
final inviteData = await scanQRImage(context);
|
||||
if (inviteData != null) {
|
||||
setState(() {
|
||||
scanned = true;
|
||||
});
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
}
|
||||
},
|
||||
child: Text(translate('scan_invitation_dialog.scan'))),
|
||||
).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
if (_scanned) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (!scanned)
|
||||
|
||||
final children = <Widget>[];
|
||||
if (isiOS || isAndroid) {
|
||||
children.addAll([
|
||||
Text(
|
||||
translate('scan_invitation_dialog.paste_qr_here'),
|
||||
translate('scan_invitation_dialog.scan_qr_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
if (!scanned)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ElevatedButton(
|
||||
onPressed: dialogState.isValidating
|
||||
? null
|
||||
: () async {
|
||||
final inviteData = await pasteQRImage(context);
|
||||
final inviteData = await scanQRImage(context);
|
||||
if (inviteData != null) {
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
setState(() {
|
||||
scanned = true;
|
||||
_scanned = true;
|
||||
});
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
}
|
||||
},
|
||||
child: Text(translate('scan_invitation_dialog.paste'))),
|
||||
child: Text(translate('scan_invitation_dialog.scan'))),
|
||||
).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
}
|
||||
|
||||
children.addAll([
|
||||
Text(
|
||||
translate('scan_invitation_dialog.paste_qr_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ElevatedButton(
|
||||
onPressed: dialogState.isValidating
|
||||
? null
|
||||
: () async {
|
||||
final inviteData = await pasteQRImage(context);
|
||||
if (inviteData != null) {
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
setState(() {
|
||||
_scanned = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Text(translate('scan_invitation_dialog.paste'))),
|
||||
).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: children);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return InvitationDialog(
|
||||
locator: widget._locator,
|
||||
onValidationCancelled: onValidationCancelled,
|
||||
onValidationSuccess: onValidationSuccess,
|
||||
onValidationFailed: onValidationFailed,
|
||||
inviteControlIsValid: inviteControlIsValid,
|
||||
buildInviteControl: buildInviteControl);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<bool>('scanned', scanned));
|
||||
}
|
||||
Widget build(BuildContext context) => InvitationDialog(
|
||||
locator: widget._locator,
|
||||
onValidationCancelled: onValidationCancelled,
|
||||
onValidationSuccess: onValidationSuccess,
|
||||
onValidationFailed: onValidationFailed,
|
||||
inviteControlIsValid: inviteControlIsValid,
|
||||
buildInviteControl: buildInviteControl);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export 'camera_qr_scanner.dart';
|
||||
export 'contact_invitation_display.dart';
|
||||
export 'contact_invitation_item_widget.dart';
|
||||
export 'contact_invitation_list_widget.dart';
|
||||
|
|
|
@ -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 'recovery_key_widget.dart';
|
||||
export 'responsive.dart';
|
||||
export 'scanner_error_widget.dart';
|
||||
export 'styled_widgets/styled_button_box.dart';
|
||||
export 'styled_widgets/styled_widgets.dart';
|
||||
export 'widget_helpers.dart';
|
||||
|
|
|
@ -6,7 +6,6 @@ import FlutterMacOS
|
|||
import Foundation
|
||||
|
||||
import file_saver
|
||||
import mobile_scanner
|
||||
import package_info_plus
|
||||
import pasteboard
|
||||
import path_provider_foundation
|
||||
|
@ -21,7 +20,6 @@ import window_manager
|
|||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
|
|
|
@ -2,9 +2,6 @@ PODS:
|
|||
- file_saver (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- pasteboard (0.0.1):
|
||||
|
@ -34,7 +31,6 @@ PODS:
|
|||
DEPENDENCIES:
|
||||
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
|
@ -52,8 +48,6 @@ EXTERNAL SOURCES:
|
|||
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
mobile_scanner:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||
pasteboard:
|
||||
|
@ -80,7 +74,6 @@ EXTERNAL SOURCES:
|
|||
SPEC CHECKSUMS:
|
||||
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
|
|
|
@ -243,7 +243,7 @@ class DHTLog implements DHTDeleteable<DHTLog> {
|
|||
/// Will execute the closure multiple times if a consistent write to the DHT
|
||||
/// is not achieved. Timeout if specified will be thrown as a
|
||||
/// TimeoutException. The closure should return a value if its changes also
|
||||
/// succeeded, and throw DHTExceptionTryAgain to trigger another
|
||||
/// succeeded, and throw DHTExceptionOutdated to trigger another
|
||||
/// eventual consistency pass.
|
||||
Future<T> operateAppendEventual<T>(
|
||||
Future<T> Function(DHTLogWriteOperations) closure,
|
||||
|
|
|
@ -250,12 +250,13 @@ class _DHTLogSpine {
|
|||
final headDelta = _ringDistance(newHead, oldHead);
|
||||
final tailDelta = _ringDistance(newTail, oldTail);
|
||||
if (headDelta > _positionLimit ~/ 2 || tailDelta > _positionLimit ~/ 2) {
|
||||
throw DHTExceptionInvalidData('_DHTLogSpine::_updateHead '
|
||||
'_head=$_head _tail=$_tail '
|
||||
'oldHead=$oldHead oldTail=$oldTail '
|
||||
'newHead=$newHead newTail=$newTail '
|
||||
'headDelta=$headDelta tailDelta=$tailDelta '
|
||||
'_positionLimit=$_positionLimit');
|
||||
throw DHTExceptionInvalidData(
|
||||
cause: '_DHTLogSpine::_updateHead '
|
||||
'_head=$_head _tail=$_tail '
|
||||
'oldHead=$oldHead oldTail=$oldTail '
|
||||
'newHead=$newHead newTail=$newTail '
|
||||
'headDelta=$headDelta tailDelta=$tailDelta '
|
||||
'_positionLimit=$_positionLimit');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
|
|||
final lookup = await _spine.lookupPosition(pos);
|
||||
if (lookup == null) {
|
||||
throw DHTExceptionInvalidData(
|
||||
'_DHTLogRead::tryWriteItem pos=$pos _spine.length=${_spine.length}');
|
||||
cause: '_DHTLogRead::tryWriteItem pos=$pos '
|
||||
'_spine.length=${_spine.length}');
|
||||
}
|
||||
|
||||
// Write item to the segment
|
||||
|
@ -75,10 +76,11 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations {
|
|||
|
||||
final lookup = await _spine.lookupPosition(insertPos + valueIdx);
|
||||
if (lookup == null) {
|
||||
throw DHTExceptionInvalidData('_DHTLogWrite::addAll '
|
||||
'_spine.length=${_spine.length}'
|
||||
'insertPos=$insertPos valueIdx=$valueIdx '
|
||||
'values.length=${values.length} ');
|
||||
throw DHTExceptionInvalidData(
|
||||
cause: '_DHTLogWrite::addAll '
|
||||
'_spine.length=${_spine.length}'
|
||||
'insertPos=$insertPos valueIdx=$valueIdx '
|
||||
'values.length=${values.length} ');
|
||||
}
|
||||
|
||||
final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos);
|
||||
|
|
|
@ -531,7 +531,7 @@ class _DHTShortArrayHead {
|
|||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Head/element mutex to ensure we keep the representation valid
|
||||
final Mutex _headMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null);
|
||||
final _headMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null);
|
||||
// Subscription to head record internal changes
|
||||
StreamSubscription<DHTRecordWatchChange>? _subscription;
|
||||
// Notify closure for external head changes
|
||||
|
|
|
@ -15,27 +15,37 @@ abstract class DHTAdd {
|
|||
Future<void> add(Uint8List value);
|
||||
|
||||
/// Try to add a list of items to the DHT container.
|
||||
/// Return if the elements were successfully added.
|
||||
/// Throws DHTExceptionTryAgain if the state changed before the elements could
|
||||
/// Return the number of elements successfully added.
|
||||
/// Throws DHTExceptionTryAgain if the state changed before any elements could
|
||||
/// be added or a newer value was found on the network.
|
||||
/// Throws DHTConcurrencyLimit if the number values in the list was too large
|
||||
/// at this time
|
||||
/// Throws a StateError if the container exceeds its maximum size.
|
||||
Future<void> addAll(List<Uint8List> values);
|
||||
}
|
||||
|
||||
extension DHTAddExt on DHTAdd {
|
||||
/// Convenience function:
|
||||
/// Like tryAddItem but also encodes the input value as JSON and parses the
|
||||
/// returned element as JSON
|
||||
/// Like add but also encodes the input value as JSON
|
||||
Future<void> addJson<T>(
|
||||
T newValue,
|
||||
) =>
|
||||
add(jsonEncodeBytes(newValue));
|
||||
|
||||
/// Convenience function:
|
||||
/// Like tryAddItem but also encodes the input value as a protobuf object
|
||||
/// and parses the returned element as a protobuf object
|
||||
/// Like add but also encodes the input value as a protobuf object
|
||||
Future<void> addProtobuf<T extends GeneratedMessage>(
|
||||
T newValue,
|
||||
) =>
|
||||
add(newValue.writeToBuffer());
|
||||
|
||||
/// Convenience function:
|
||||
/// Like addAll but also encodes the input values as JSON
|
||||
Future<void> addAllJson<T>(List<T> values) =>
|
||||
addAll(values.map(jsonEncodeBytes).toList());
|
||||
|
||||
/// Convenience function:
|
||||
/// Like addAll but also encodes the input values as protobuf objects
|
||||
Future<void> addAllProtobuf<T extends GeneratedMessage>(List<T> values) =>
|
||||
addAll(values.map((x) => x.writeToBuffer()).toList());
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
const DHTExceptionOutdated(
|
||||
[this.cause = 'operation failed due to newer dht value']);
|
||||
{this.cause = 'operation failed due to newer dht value'});
|
||||
final String cause;
|
||||
|
||||
@override
|
||||
String toString() => 'DHTExceptionOutdated: $cause';
|
||||
}
|
||||
|
||||
class DHTConcurrencyLimit implements Exception {
|
||||
const DHTConcurrencyLimit(
|
||||
{required this.limit,
|
||||
this.cause = 'failed due to maximum parallel operation limit'});
|
||||
final String cause;
|
||||
final int limit;
|
||||
|
||||
@override
|
||||
String toString() => 'DHTConcurrencyLimit: $cause (limit=$limit)';
|
||||
}
|
||||
|
||||
class DHTExceptionInvalidData implements Exception {
|
||||
const DHTExceptionInvalidData(this.cause);
|
||||
const DHTExceptionInvalidData({this.cause = 'data was invalid'});
|
||||
final String cause;
|
||||
|
||||
@override
|
||||
|
@ -16,7 +27,7 @@ class DHTExceptionInvalidData implements Exception {
|
|||
}
|
||||
|
||||
class DHTExceptionCancelled implements Exception {
|
||||
const DHTExceptionCancelled([this.cause = 'operation was cancelled']);
|
||||
const DHTExceptionCancelled({this.cause = 'operation was cancelled'});
|
||||
final String cause;
|
||||
|
||||
@override
|
||||
|
@ -25,7 +36,7 @@ class DHTExceptionCancelled implements Exception {
|
|||
|
||||
class DHTExceptionNotAvailable implements Exception {
|
||||
const DHTExceptionNotAvailable(
|
||||
[this.cause = 'request could not be completed at this time']);
|
||||
{this.cause = 'request could not be completed at this time'});
|
||||
final String cause;
|
||||
|
||||
@override
|
||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -274,7 +274,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.3.1"
|
||||
camera:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera
|
||||
sha256: "413d2b34fe28496c35c69ede5b232fb9dd5ca2c3a4cb606b14efc1c7546cc8cb"
|
||||
|
@ -394,7 +394,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
|
@ -912,14 +912,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
mobile_scanner:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mobile_scanner
|
||||
sha256: "72f06a071aa8b14acea3ab43ea7949eefe4a2469731ae210e006ba330a033a8c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1765,7 +1757,7 @@ packages:
|
|||
path: "../veilid/veilid-flutter"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.4.6"
|
||||
version: "0.4.7"
|
||||
veilid_support:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -23,10 +23,12 @@ dependencies:
|
|||
bloc: ^9.0.0
|
||||
bloc_advanced_tools: ^0.1.13
|
||||
blurry_modal_progress_hud: ^1.1.1
|
||||
camera: ^0.11.1
|
||||
change_case: ^2.2.0
|
||||
charcode: ^1.4.0
|
||||
circular_profile_avatar: ^2.0.5
|
||||
circular_reveal_animation: ^2.0.1
|
||||
convert: ^3.1.2
|
||||
cupertino_icons: ^1.0.8
|
||||
equatable: ^2.0.7
|
||||
expansion_tile_group: ^2.2.0
|
||||
|
@ -58,7 +60,6 @@ dependencies:
|
|||
json_annotation: ^4.9.0
|
||||
loggy: ^2.0.3
|
||||
meta: ^1.16.0
|
||||
mobile_scanner: ^7.0.0
|
||||
package_info_plus: ^8.3.0
|
||||
pasteboard: ^0.4.0
|
||||
path: ^1.9.1
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue