diff --git a/android/gradle.properties b/android/gradle.properties
index 84044a9..d636853 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,3 +1,5 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+
android.enableJetifier=false
+dev.steenbakker.mobile_scanner.useUnbundled=true
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 5827274..7b98ca5 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -50,5 +50,9 @@
UIApplicationSupportsIndirectInputEvents
+ NSCameraUsageDescription
+ This app needs camera access to scan QR codes
+ NSPhotoLibraryUsageDescription
+ This app needs photos access to get QR code from photo library
diff --git a/lib/view/barcode_scanner_screen.dart b/lib/view/barcode_scanner_screen.dart
new file mode 100644
index 0000000..8298808
--- /dev/null
+++ b/lib/view/barcode_scanner_screen.dart
@@ -0,0 +1,454 @@
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:mobile_scanner/mobile_scanner.dart' as scanner;
+import 'package:barcode_widget/barcode_widget.dart';
+import 'package:magicepaperapp/constants/color_constants.dart';
+import 'package:image/image.dart' as img;
+
+class BarcodeScannerScreen extends StatefulWidget {
+ final int width;
+ final int height;
+
+ const BarcodeScannerScreen({
+ super.key,
+ required this.width,
+ required this.height,
+ });
+
+ @override
+ State createState() => _BarcodeScannerScreenState();
+}
+
+class _BarcodeScannerScreenState extends State {
+ final GlobalKey _barcodeKey = GlobalKey();
+ final TextEditingController _textController = TextEditingController();
+ String _barcodeData = '';
+ bool _hasError = false;
+ Barcode _selectedBarcode = Barcode.qrCode();
+ bool _showScanner = false;
+ scanner.MobileScannerController? _scannerController;
+
+ @override
+ void initState() {
+ super.initState();
+ _textController.addListener(() {
+ setState(() {
+ _barcodeData = _textController.text;
+ _hasError = false;
+ });
+ });
+ }
+
+ final Map barcodeFormatToSupportedChars = {
+ 'Aztec': 'All',
+ 'CODABAR': '0-9 \$ - . / : +',
+ 'CODE 128': 'All',
+ 'CODE 39': '0-9 A-Z - . \$ / + % , ',
+ 'CODE 93': '0-9 A-Z - . \$ / + % , ',
+ 'Data Matrix': 'All',
+ 'EAN 13': '0-9',
+ 'EAN 2': '0-9',
+ 'EAN 5': '0-9',
+ 'EAN 8': '0-9',
+ 'GS1 128': 'All',
+ 'ISBN': '0-9',
+ 'ITF': '0-9',
+ 'ITF 14': '0-9',
+ 'ITF 16': '0-9',
+ 'PDF417': 'All',
+ 'QR Code': 'All',
+ 'RM4SCC': '0-9 A-Z',
+ 'Telepen': 'All',
+ 'UPC A': '0-9',
+ 'UPC E': '0-9',
+ };
+
+ String? _validateBarcodeData(String data, Barcode barcode) {
+ if (data.isEmpty) {
+ return null;
+ }
+ final allowedChars = barcode.charSet.toSet();
+ for (final rune in data.runes) {
+ if (!allowedChars.contains(rune)) {
+ final char = String.fromCharCode(rune);
+ final rules = barcodeFormatToSupportedChars[_selectedBarcode.name];
+ return "Invalid character '$char' \nSupported characters are ${rules ?? 'Please check the barcode rules.'}";
+ }
+ }
+ if (data.length < barcode.minLength) {
+ return 'Data is too short. Minimum length for ${barcode.name} is ${barcode.minLength}.';
+ }
+ if (barcode.maxLength < 10000 && data.length > barcode.maxLength) {
+ return 'Data is too long. Maximum length for ${barcode.name} is ${barcode.maxLength}.';
+ }
+ return null;
+ }
+
+ void _handleBarcode(scanner.BarcodeCapture barcodes) {
+ if (mounted && barcodes.barcodes.isNotEmpty) {
+ final barcode = barcodes.barcodes.first;
+ final value = barcode.displayValue ?? barcode.rawValue ?? '';
+
+ setState(() {
+ _textController.text = value;
+ _barcodeData = value;
+ _selectedBarcode = _getBarcodeType(barcode.format);
+ _showScanner = false;
+ });
+
+ _scannerController?.dispose();
+ _scannerController = null;
+ }
+ }
+
+ @override
+ void dispose() {
+ _textController.dispose();
+
+ _scannerController?.dispose();
+
+ super.dispose();
+ }
+
+ void _startScanning() {
+ setState(() {
+ _showScanner = true;
+ _scannerController = scanner.MobileScannerController();
+ });
+ }
+
+ void _stopScanning() {
+ setState(() {
+ _showScanner = false;
+ });
+ _scannerController?.dispose();
+ _scannerController = null;
+ }
+
+ Barcode _getBarcodeType(scanner.BarcodeFormat? format) {
+ switch (format) {
+ case scanner.BarcodeFormat.qrCode:
+ return Barcode.qrCode();
+ case scanner.BarcodeFormat.code128:
+ return Barcode.code128();
+ case scanner.BarcodeFormat.code39:
+ return Barcode.code39();
+ case scanner.BarcodeFormat.code93:
+ return Barcode.code93();
+ case scanner.BarcodeFormat.ean13:
+ return Barcode.ean13();
+ case scanner.BarcodeFormat.ean8:
+ return Barcode.ean8();
+ case scanner.BarcodeFormat.upcA:
+ return Barcode.upcA();
+ case scanner.BarcodeFormat.upcE:
+ return Barcode.upcE();
+ case scanner.BarcodeFormat.dataMatrix:
+ return Barcode.dataMatrix();
+ case scanner.BarcodeFormat.pdf417:
+ return Barcode.pdf417();
+ case scanner.BarcodeFormat.aztec:
+ return Barcode.aztec();
+ case scanner.BarcodeFormat.codabar:
+ return Barcode.codabar();
+ case scanner.BarcodeFormat.itf:
+ return Barcode.itf();
+ default:
+ return Barcode.code128();
+ }
+ }
+
+ Future _generateBarcodeImage() async {
+ RenderRepaintBoundary boundary =
+ _barcodeKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
+ ui.Image image = await boundary.toImage(pixelRatio: 3.0);
+ ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
+ Uint8List pngBytes = byteData!.buffer.asUint8List();
+
+ img.Image? barcodeImage = img.decodeImage(pngBytes);
+ if (barcodeImage == null) return;
+
+ img.Image resizedImage;
+ if (barcodeImage.width > widget.width) {
+ resizedImage = img.copyRotate(barcodeImage, angle: 270);
+ } else {
+ resizedImage = img.copyResize(barcodeImage,
+ width: widget.width, height: widget.height);
+ }
+ final resultBytes = Uint8List.fromList(img.encodePng(resizedImage));
+ Navigator.of(context).pop(resultBytes);
+ }
+
+ Widget _buildFormatSelector() {
+ final Map availableFormats = {
+ 'QR Code': Barcode.qrCode(),
+ 'Data Matrix': Barcode.dataMatrix(),
+ 'Aztec': Barcode.aztec(),
+ 'PDF417': Barcode.pdf417(),
+ 'Code 128': Barcode.code128(),
+ 'Code 93': Barcode.code93(),
+ 'Code 39': Barcode.code39(),
+ 'Codabar': Barcode.codabar(),
+ 'EAN-13': Barcode.ean13(),
+ 'EAN-8': Barcode.ean8(),
+ 'EAN-5': Barcode.ean5(),
+ 'EAN-2': Barcode.ean2(),
+ 'GS1 128': Barcode.gs128(),
+ 'ISBN': Barcode.isbn(),
+ 'ITF': Barcode.itf(),
+ 'ITF-16': Barcode.itf16(),
+ 'ITF-14': Barcode.itf14(),
+ 'RM4SCC': Barcode.rm4scc(),
+ 'Telepen': Barcode.telepen(),
+ 'UPC-A': Barcode.upcA(),
+ 'UPC-E': Barcode.upcE(),
+ };
+
+ return DropdownButtonFormField(
+ value: _selectedBarcode.name,
+ decoration: const InputDecoration(
+ labelText: 'Barcode Format',
+ border: OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.red),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.red, width: 2),
+ ),
+ ),
+ items: availableFormats.entries
+ .map((entry) => DropdownMenuItem(
+ value: entry.value.name,
+ child: Text(entry.key),
+ ))
+ .toList(),
+ onChanged: (newBarcodeName) {
+ if (newBarcodeName != null) {
+ setState(() {
+ _selectedBarcode = availableFormats.values.firstWhere(
+ (barcode) => barcode.name == newBarcodeName,
+ orElse: () => Barcode.qrCode(),
+ );
+ _hasError = false;
+ });
+ }
+ },
+ );
+ }
+
+ Widget _buildBarcodePreview() {
+ if (_barcodeData.isEmpty) {
+ return Container(
+ width: 240,
+ height: 120,
+ decoration: BoxDecoration(
+ color: Colors.grey[200],
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Center(
+ child: Text(
+ 'Enter or scan barcode data',
+ style: TextStyle(color: Colors.grey),
+ ),
+ ),
+ );
+ }
+
+ return BarcodeWidget(
+ errorBuilder: (context, error) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ setState(() {
+ _hasError = true;
+ });
+ });
+
+ final validationError =
+ _validateBarcodeData(_barcodeData, _selectedBarcode);
+ if (validationError != null) {
+ error = validationError;
+ }
+
+ return Container(
+ width: double.infinity,
+ height: 250,
+ decoration: BoxDecoration(
+ color: Colors.red[50],
+ border: Border.all(color: Colors.red[400]!, width: 2),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.error_outline,
+ color: Colors.red[600],
+ size: 45,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Invalid Barcode',
+ style: TextStyle(
+ color: Colors.red[700],
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Expanded(
+ child: SingleChildScrollView(
+ child: Text(
+ error.toString(),
+ style: TextStyle(
+ color: Colors.red[600],
+ fontSize: 18,
+ fontWeight: FontWeight.w500,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ style: const TextStyle(color: colorBlack),
+ padding: const EdgeInsets.all(10),
+ backgroundColor: colorWhite,
+ barcode: _selectedBarcode,
+ data: _barcodeData,
+ );
+ }
+
+ Widget _buildScannerView() {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Scan Barcode'),
+ backgroundColor: colorAccent,
+ leading: IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: _stopScanning,
+ ),
+ ),
+ backgroundColor: colorBlack,
+ body: Stack(
+ children: [
+ scanner.MobileScanner(
+ controller: _scannerController,
+ onDetect: _handleBarcode,
+ ),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ alignment: Alignment.bottomCenter,
+ height: 100,
+ color: const Color.fromRGBO(0, 0, 0, 0.4),
+ child: const Center(
+ child: Text(
+ 'Point camera at barcode to scan',
+ style: TextStyle(color: colorWhite, fontSize: 16),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_showScanner) {
+ return _buildScannerView();
+ }
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Barcode Generator'),
+ titleTextStyle: const TextStyle(
+ color: Colors.white,
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ backgroundColor: colorAccent,
+ ),
+ body: SingleChildScrollView(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const SizedBox(height: 20),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ TextField(
+ controller: _textController,
+ decoration: const InputDecoration(
+ labelText: 'Barcode Data',
+ hintText: 'Enter barcode data or scan',
+ prefixIcon: Icon(Icons.qr_code_2_rounded),
+ border: OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.red),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.red, width: 2),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(top: 8.0, left: 4.0),
+ child: Text(
+ 'Characters: ${_barcodeData.length}',
+ style: TextStyle(
+ color: Colors.grey[600],
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 20),
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton.icon(
+ onPressed: _startScanning,
+ icon: const Icon(Icons.qr_code_scanner),
+ label: const Text('Scan Barcode'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: colorAccent,
+ foregroundColor: colorWhite,
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ ),
+ ),
+ ),
+ const SizedBox(height: 40),
+ if (_barcodeData.isNotEmpty) _buildFormatSelector(),
+ RepaintBoundary(
+ key: _barcodeKey,
+ child: Center(child: _buildBarcodePreview()),
+ ),
+ const SizedBox(height: 20),
+ if (_barcodeData.isNotEmpty && !_hasError)
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: _generateBarcodeImage,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.green,
+ foregroundColor: colorWhite,
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ ),
+ child: const Text('Generate Image'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/view/image_editor.dart b/lib/view/image_editor.dart
index 4ffbb7e..7a430c9 100644
--- a/lib/view/image_editor.dart
+++ b/lib/view/image_editor.dart
@@ -12,6 +12,8 @@ import 'package:magicepaperapp/util/epd/driver/waveform.dart';
import 'package:magicepaperapp/util/image_editor_utils.dart';
import 'package:magicepaperapp/util/xbm_encoder.dart';
import 'package:magicepaperapp/view/widget/image_list.dart';
+import 'package:magicepaperapp/view/barcode_scanner_screen.dart';
+
import 'package:pro_image_editor/pro_image_editor.dart';
import 'package:provider/provider.dart';
import 'package:image/image.dart' as img;
@@ -511,6 +513,30 @@ class BottomActionMenu extends StatelessWidget {
await imageSaveHandler?.navigateToImageLibrary();
},
),
+ _buildActionButton(
+ context: context,
+ icon: Icons.qr_code_scanner,
+ label: 'Barcode',
+ onTap: () async {
+ final result = await Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (context) => BarcodeScannerScreen(
+ width: epd.width,
+ height: epd.height,
+ ),
+ ),
+ );
+
+ if (result is Uint8List) {
+ await imgLoader.updateImage(
+ bytes: result,
+ width: epd.width,
+ height: epd.height,
+ );
+ await imgLoader.saveFinalizedImageBytes(result);
+ }
+ },
+ ),
_buildActionButton(
context: context,
icon: Icons.dashboard_customize_outlined,
@@ -532,6 +558,7 @@ class BottomActionMenu extends StatelessWidget {
height: epd.height,
);
await imgLoader.saveFinalizedImageBytes(result);
+
onSourceChanged?.call('template');
}
},
diff --git a/pubspec.yaml b/pubspec.yaml
index a9a864b..c711538 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -39,12 +39,13 @@ dependencies:
file_picker: ^10.1.2
flutter_colorpicker: ^1.1.0
mime: ^2.0.0
+ mobile_scanner: ^7.0.1
+ barcode_widget: ^2.0.4
intl: ^0.20.2
path_provider: ^2.0.15
get_it: ^8.0.3
path: ^1.9.1
permission_handler: ^11.0.1
- barcode_widget: ^2.0.4
ndef: ^0.3.1
dev_dependencies: