twonly-app-dependencies/qr_flutter/lib/src/qr_image_view.dart
2025-12-08 00:06:11 +01:00

336 lines
10 KiB
Dart

/*
* QR.Flutter
* Copyright (c) 2019 the QR.Flutter authors.
* See LICENSE for distribution and usage details.
*/
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:qr/qr.dart';
import 'qr_painter.dart';
import 'qr_versions.dart';
import 'types.dart';
import 'validator.dart';
/// A widget that shows a QR code.
class QrImageView extends StatefulWidget {
/// Create a new QR code using the [String] data and the passed options (or
/// using the default options).
QrImageView({
required String data,
super.key,
this.size,
this.padding = const EdgeInsets.all(10.0),
this.backgroundColor = Colors.transparent,
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
this.foregroundColor = Colors.black,
this.version = QrVersions.auto,
this.errorCorrectionLevel = QrErrorCorrectLevel.L,
this.errorStateBuilder,
this.constrainErrorBounds = true,
this.gapless = true,
this.embeddedImage,
this.embeddedImageStyle = const QrEmbeddedImageStyle(),
this.semanticsLabel = 'qr code',
this.eyeStyle = const QrEyeStyle(
eyeShape: QrEyeShape.square,
),
this.dataModuleStyle = const QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
),
this.embeddedImageEmitsError = false,
this.gradient,
}) : assert(
QrVersions.isSupportedVersion(version),
'QR code version $version is not supported',
),
_data = data,
_qrCode = null;
/// Create a new QR code using the [QrCode] data and the passed options (or
/// using the default options).
QrImageView.withQr({
required QrCode qr,
super.key,
this.size,
this.padding = const EdgeInsets.all(10.0),
this.backgroundColor = Colors.transparent,
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
this.foregroundColor = Colors.black,
this.version = QrVersions.auto,
this.errorCorrectionLevel = QrErrorCorrectLevel.L,
this.errorStateBuilder,
this.constrainErrorBounds = true,
this.gapless = true,
this.embeddedImage,
this.embeddedImageStyle = const QrEmbeddedImageStyle(),
this.semanticsLabel = 'qr code',
this.eyeStyle = const QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Colors.black,
),
this.dataModuleStyle = const QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Colors.black,
),
this.embeddedImageEmitsError = false,
this.gradient,
}) : assert(
QrVersions.isSupportedVersion(version),
'QR code version $version is not supported',
),
_data = null,
_qrCode = qr;
// The data passed to the widget
final String? _data;
// The QR code data passed to the widget
final QrCode? _qrCode;
/// The background color of the final QR code widget.
final Color backgroundColor;
/// The foreground color of the final QR code widget.
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
final Color foregroundColor;
/// The gradient for all (dataModule and eye)
final Gradient? gradient;
/// The QR code version to use.
final int version;
/// The QR code error correction level to use.
final int errorCorrectionLevel;
/// The external padding between the edge of the widget and the content.
final EdgeInsets padding;
/// The intended size of the widget.
final double? size;
/// The callback that is executed in the event of an error so that you can
/// interrogate the exception and construct an alternative view to present
/// to your user.
final QrErrorBuilder? errorStateBuilder;
/// If `true` then the error widget will be constrained to the boundary of the
/// QR widget if it had been valid. If `false` the error widget will grow to
/// the size it needs. If the error widget is allowed to grow, your layout may
/// jump around (depending on specifics).
///
/// NOTE: Setting a [size] value will override this setting and both the
/// content widget and error widget will adhere to the size value.
final bool constrainErrorBounds;
/// If set to false, each of the squares in the QR code will have a small
/// gap. Default is true.
final bool gapless;
/// The image data to embed (as an overlay) in the QR code. The image will
/// be added to the center of the QR code.
final ImageProvider? embeddedImage;
/// Styling options for the image overlay.
final QrEmbeddedImageStyle embeddedImageStyle;
/// If set to true and there is an error loading the embedded image, the
/// [errorStateBuilder] callback will be called (if it is defined). If false,
/// the widget will ignore the embedded image and just display the QR code.
/// The default is false.
final bool embeddedImageEmitsError;
/// [semanticsLabel] will be used by screen readers to describe the content of
/// the qr code.
/// Default is 'qr code'.
final String semanticsLabel;
/// Styling option for QR Eye ball and frame.
final QrEyeStyle eyeStyle;
/// Styling option for QR data module.
final QrDataModuleStyle dataModuleStyle;
@override
State<QrImageView> createState() => _QrImageViewState();
}
class _QrImageViewState extends State<QrImageView> {
/// The QR code string data.
QrCode? _qr;
/// The current validation status.
late QrValidationResult _validationResult;
@override
Widget build(BuildContext context) {
if (widget._data != null) {
_validationResult = QrValidator.validate(
data: widget._data!,
version: widget.version,
errorCorrectionLevel: widget.errorCorrectionLevel,
);
_qr = _validationResult.isValid ? _validationResult.qrCode : null;
} else if (widget._qrCode != null) {
_qr = widget._qrCode;
_validationResult =
QrValidationResult(status: QrValidationStatus.valid, qrCode: _qr);
}
return LayoutBuilder(
builder: (context, constraints) {
// validation failed, show an error state widget if builder is present.
if (!_validationResult.isValid) {
return _errorWidget(context, constraints, _validationResult.error);
}
// no error, build the regular widget
final widgetSize =
widget.size ?? constraints.biggest.shortestSide;
if (widget.embeddedImage != null) {
// if requesting to embed an image then we need to load via a
// FutureBuilder because the image provider will be async.
return FutureBuilder<ui.Image>(
future: _loadQrImage(context, widget.embeddedImageStyle),
builder: (ctx, snapshot) {
if (snapshot.error != null) {
debugPrint('snapshot error: ${snapshot.error}');
return widget.embeddedImageEmitsError
? _errorWidget(context, constraints, snapshot.error)
: _qrWidget(null, widgetSize);
}
if (snapshot.hasData) {
debugPrint('loaded image');
final loadedImage = snapshot.data;
return _qrWidget(loadedImage, widgetSize);
} else {
return Container();
}
},
);
} else {
return _qrWidget(null, widgetSize);
}
},
);
}
Widget _qrWidget(ui.Image? image, double edgeLength) {
final painter = QrPainter.withQr(
qr: _qr!,
// ignore: deprecated_member_use_from_same_package
color: widget.foregroundColor,
gapless: widget.gapless,
embeddedImageStyle: widget.embeddedImageStyle,
embeddedImage: image,
eyeStyle: widget.eyeStyle,
dataModuleStyle: widget.dataModuleStyle,
gradient: widget.gradient
);
return _QrContentView(
edgeLength: edgeLength,
backgroundColor: widget.backgroundColor,
padding: widget.padding,
semanticsLabel: widget.semanticsLabel,
child: CustomPaint(painter: painter),
);
}
Widget _errorWidget(
BuildContext context,
BoxConstraints constraints,
Object? error,
) {
final errorWidget = widget.errorStateBuilder == null
? Container()
: widget.errorStateBuilder!(context, error);
final errorSideLength = widget.constrainErrorBounds
? widget.size ?? constraints.biggest.shortestSide
: constraints.biggest.longestSide;
return _QrContentView(
edgeLength: errorSideLength,
backgroundColor: widget.backgroundColor,
padding: widget.padding,
semanticsLabel: widget.semanticsLabel,
child: errorWidget,
);
}
late ImageStreamListener streamListener;
Future<ui.Image> _loadQrImage(
BuildContext buildContext,
QrEmbeddedImageStyle? style,
) {
if (style != null) {}
final mq = MediaQuery.of(buildContext);
final completer = Completer<ui.Image>();
final stream = widget.embeddedImage!.resolve(
ImageConfiguration(
devicePixelRatio: mq.devicePixelRatio,
),
);
streamListener = ImageStreamListener(
(info, err) {
stream.removeListener(streamListener);
completer.complete(info.image);
},
onError: (err, _) {
stream.removeListener(streamListener);
completer.completeError(err);
},
);
stream.addListener(streamListener);
return completer.future;
}
}
/// A function type to be called when any form of error occurs while
/// painting a [QrImageView].
typedef QrErrorBuilder = Widget Function(BuildContext context, Object? error);
class _QrContentView extends StatelessWidget {
const _QrContentView({
required this.edgeLength,
required this.child,
this.backgroundColor,
this.padding,
this.semanticsLabel,
});
/// The length of both edges (because it has to be a square).
final double edgeLength;
/// The background color of the containing widget.
final Color? backgroundColor;
/// The padding that surrounds the child widget.
final EdgeInsets? padding;
/// The child widget.
final Widget child;
/// [semanticsLabel] will be used by screen readers to describe the content of
/// the qr code.
final String? semanticsLabel;
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticsLabel,
child: Container(
width: edgeLength,
height: edgeLength,
color: backgroundColor,
child: Padding(
padding: padding!,
child: child,
),
),
);
}
}