You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1090 lines
35 KiB
1090 lines
35 KiB
import 'dart:typed_data';
|
|
import 'dart:io' show Platform, File;
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:pdf/pdf.dart';
|
|
import 'package:pdf/widgets.dart' as pw;
|
|
import 'package:printing/printing.dart';
|
|
import 'package:qr_flutter/qr_flutter.dart';
|
|
|
|
// Classe pour gérer les formats d'étiquettes Niimbot
|
|
class NiimbotLabelSize {
|
|
final double width;
|
|
final double height;
|
|
final String name;
|
|
|
|
const NiimbotLabelSize(this.width, this.height, this.name);
|
|
|
|
// Formats courants Niimbot B1
|
|
static const small = NiimbotLabelSize(50, 15, "Petit (50x15mm)");
|
|
static const medium = NiimbotLabelSize(40, 30, "Moyen (40x30mm)");
|
|
static const large = NiimbotLabelSize(50, 30, "Grand (50x30mm)");
|
|
|
|
PdfPageFormat get pageFormat => PdfPageFormat(
|
|
width * PdfPageFormat.mm,
|
|
height * PdfPageFormat.mm,
|
|
marginAll: 0,
|
|
);
|
|
}
|
|
|
|
class PdfPrintService {
|
|
pw.Font? robotoRegular;
|
|
pw.Font? robotoBold;
|
|
|
|
PdfPrintService();
|
|
|
|
/// Charge les polices Unicode
|
|
Future<void> loadFonts() async {
|
|
if (robotoRegular != null && robotoBold != null) return;
|
|
|
|
try {
|
|
// Charger les polices depuis les assets ou utiliser les polices système
|
|
final regularData = await rootBundle.load('assets/fonts/Roboto-Regular.ttf');
|
|
final boldData = await rootBundle.load('assets/fonts/Roboto-Bold.ttf');
|
|
|
|
robotoRegular = pw.Font.ttf(regularData);
|
|
robotoBold = pw.Font.ttf(boldData);
|
|
} catch (e) {
|
|
print('Erreur chargement polices personnalisées: $e');
|
|
// Fallback vers les polices par défaut
|
|
try {
|
|
robotoRegular = await PdfGoogleFonts.robotoRegular();
|
|
robotoBold = await PdfGoogleFonts.robotoBold();
|
|
} catch (e2) {
|
|
print('Erreur chargement polices Google: $e2');
|
|
// Utiliser les polices système par défaut
|
|
robotoRegular = null;
|
|
robotoBold = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Génère une image QR optimisée
|
|
Future<Uint8List> _generateQrImage(String data, {double size = 512}) async {
|
|
final qrValidation = QrValidator.validate(
|
|
data: data,
|
|
version: QrVersions.auto,
|
|
errorCorrectionLevel: QrErrorCorrectLevel.M,
|
|
);
|
|
|
|
if (qrValidation.status != QrValidationStatus.valid) {
|
|
throw Exception('QR code invalide: ${qrValidation.error}');
|
|
}
|
|
|
|
final qrCode = qrValidation.qrCode!;
|
|
final painter = QrPainter.withQr(
|
|
qr: qrCode,
|
|
color: Colors.black,
|
|
emptyColor: Colors.white,
|
|
gapless: true,
|
|
embeddedImageStyle: null,
|
|
eyeStyle: const QrEyeStyle(
|
|
eyeShape: QrEyeShape.square,
|
|
color: Colors.black,
|
|
),
|
|
dataModuleStyle: const QrDataModuleStyle(
|
|
dataModuleShape: QrDataModuleShape.square,
|
|
color: Colors.black,
|
|
),
|
|
);
|
|
|
|
final picData = await painter.toImageData(
|
|
size,
|
|
format: ImageByteFormat.png
|
|
);
|
|
return picData!.buffer.asUint8List();
|
|
}
|
|
|
|
/// Génère le PDF optimisé pour l'impression d'étiquettes
|
|
Future<Uint8List> generateQrPdf(String data, {
|
|
String? productName,
|
|
String? reference,
|
|
bool isLabelFormat = true,
|
|
}) async {
|
|
await loadFonts();
|
|
|
|
final qrImageData = await _generateQrImage(data, size: 300);
|
|
final qrImage = pw.MemoryImage(qrImageData);
|
|
|
|
final pdf = pw.Document();
|
|
|
|
if (isLabelFormat) {
|
|
// Format étiquette (petit format)
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: const PdfPageFormat(
|
|
2.5 * PdfPageFormat.inch,
|
|
1.5 * PdfPageFormat.inch,
|
|
marginAll: 0,
|
|
),
|
|
build: (context) => _buildLabelContent(qrImage, data, productName, reference),
|
|
),
|
|
);
|
|
} else {
|
|
// Format A4 standard
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat.a4,
|
|
margin: const pw.EdgeInsets.all(20),
|
|
build: (context) => _buildA4Content(qrImage, data, productName, reference),
|
|
),
|
|
);
|
|
}
|
|
|
|
return pdf.save();
|
|
}
|
|
|
|
/// Contenu pour format étiquette
|
|
pw.Widget _buildLabelContent(
|
|
pw.MemoryImage qrImage,
|
|
String data,
|
|
String? productName,
|
|
String? reference,
|
|
) {
|
|
return pw.Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
padding: const pw.EdgeInsets.all(4),
|
|
child: pw.Center(
|
|
child: pw.Column(
|
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
// QR Code principal
|
|
pw.Container(
|
|
width: 70,
|
|
height: 70,
|
|
child: pw.Center(
|
|
child: pw.Image(
|
|
qrImage,
|
|
width: 70,
|
|
height: 70,
|
|
fit: pw.BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
|
|
pw.SizedBox(height: 4),
|
|
|
|
// Informations textuelles centrées
|
|
if (reference != null) ...[
|
|
pw.Container(
|
|
width: double.infinity,
|
|
child: pw.Text(
|
|
reference,
|
|
style: _getTextStyle(fontSize: 8, bold: true),
|
|
textAlign: pw.TextAlign.center,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
pw.SizedBox(height: 2),
|
|
],
|
|
if (productName != null && productName.isNotEmpty) ...[
|
|
pw.Container(
|
|
width: double.infinity,
|
|
child: pw.Text(
|
|
productName.length > 20 ? '${productName.substring(0, 17)}...' : productName,
|
|
style: _getTextStyle(fontSize: 6),
|
|
textAlign: pw.TextAlign.center,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Contenu pour format A4
|
|
pw.Widget _buildA4Content(
|
|
pw.MemoryImage qrImage,
|
|
String data,
|
|
String? productName,
|
|
String? reference,
|
|
) {
|
|
return pw.Center(
|
|
child: pw.Column(
|
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
|
children: [
|
|
if (productName != null) ...[
|
|
pw.Text(
|
|
productName,
|
|
style: _getTextStyle(fontSize: 18, bold: true),
|
|
textAlign: pw.TextAlign.center,
|
|
),
|
|
pw.SizedBox(height: 20),
|
|
],
|
|
|
|
pw.Image(
|
|
qrImage,
|
|
width: 200,
|
|
height: 200,
|
|
fit: pw.BoxFit.contain,
|
|
),
|
|
|
|
pw.SizedBox(height: 20),
|
|
|
|
if (reference != null) ...[
|
|
pw.Text(
|
|
'Référence: $reference',
|
|
style: _getTextStyle(fontSize: 14, bold: true),
|
|
),
|
|
pw.SizedBox(height: 10),
|
|
],
|
|
|
|
pw.Text(
|
|
'URL: $data',
|
|
style: _getTextStyle(fontSize: 12),
|
|
textAlign: pw.TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Obtient le style de texte approprié
|
|
pw.TextStyle _getTextStyle({double fontSize = 12, bool bold = false}) {
|
|
if (bold && robotoBold != null) {
|
|
return pw.TextStyle(font: robotoBold!, fontSize: fontSize);
|
|
} else if (!bold && robotoRegular != null) {
|
|
return pw.TextStyle(font: robotoRegular!, fontSize: fontSize);
|
|
} else {
|
|
return pw.TextStyle(fontSize: fontSize, fontWeight: bold ? pw.FontWeight.bold : pw.FontWeight.normal);
|
|
}
|
|
}
|
|
|
|
/// Imprime le QR code avec options
|
|
Future<void> printQr(
|
|
String data, {
|
|
String? productName,
|
|
String? reference,
|
|
bool isLabelFormat = true,
|
|
bool showPreview = true,
|
|
}) async {
|
|
try {
|
|
final pdfBytes = await generateQrPdf(
|
|
data,
|
|
productName: productName,
|
|
reference: reference,
|
|
isLabelFormat: isLabelFormat,
|
|
);
|
|
|
|
if (Platform.isWindows || Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
|
|
await Printing.layoutPdf(
|
|
onLayout: (format) async => pdfBytes,
|
|
name: 'QR_Code_${reference ?? 'etiquette'}',
|
|
format: isLabelFormat ?
|
|
const PdfPageFormat(
|
|
2.5 * PdfPageFormat.inch,
|
|
1.5 * PdfPageFormat.inch,
|
|
marginAll: 0,
|
|
) : PdfPageFormat.a4,
|
|
usePrinterSettings: false,
|
|
);
|
|
|
|
Get.snackbar(
|
|
'Succès',
|
|
'QR Code envoyé à l\'imprimante',
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
} else {
|
|
Get.snackbar(
|
|
'Info',
|
|
'Impression non supportée sur cette plateforme',
|
|
backgroundColor: Colors.orange,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print('Erreur impression: $e');
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Impossible d\'imprimer: ${e.toString()}',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ===== NOUVELLES MÉTHODES OPTIMISÉES POUR NIIMBOT B1 =====
|
|
Future<void> printQrNiimbotOptimized(
|
|
String data, {
|
|
String? productName,
|
|
String? reference,
|
|
double leftPadding = 1.0,
|
|
double topPadding = 0.5,
|
|
double qrSize = 11.5, // légèrement plus petit pour laisser la place au texte
|
|
double fontSize = 5.0,
|
|
NiimbotLabelSize labelSize = NiimbotLabelSize.small,
|
|
}) async {
|
|
try {
|
|
await loadFonts();
|
|
|
|
// Générer le QR code en image mémoire
|
|
final qrImageData = await _generateQrImage(data, size: 110);
|
|
final qrImage = pw.MemoryImage(qrImageData);
|
|
|
|
final pdf = pw.Document();
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: labelSize.pageFormat,
|
|
margin: pw.EdgeInsets.zero,
|
|
build: (context) {
|
|
return pw.Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
padding: pw.EdgeInsets.only(
|
|
left: leftPadding * PdfPageFormat.mm,
|
|
top: topPadding * PdfPageFormat.mm,
|
|
right: 1 * PdfPageFormat.mm,
|
|
bottom: 0.3 * PdfPageFormat.mm, // bordure minimale
|
|
),
|
|
child: pw.Column(
|
|
mainAxisAlignment: pw.MainAxisAlignment.start,
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
// --- QR code ---
|
|
pw.Container(
|
|
width: qrSize * PdfPageFormat.mm,
|
|
height: qrSize * PdfPageFormat.mm,
|
|
child: pw.Image(qrImage, fit: pw.BoxFit.contain),
|
|
),
|
|
|
|
// --- Référence directement sous le QR ---
|
|
if (reference != null && reference.isNotEmpty)
|
|
pw.Padding(
|
|
padding: pw.EdgeInsets.only(top: 0.3 * PdfPageFormat.mm),
|
|
child: pw.Text(
|
|
reference,
|
|
style: pw.TextStyle(
|
|
fontSize: fontSize,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
textAlign: pw.TextAlign.center,
|
|
maxLines: 1,
|
|
overflow: pw.TextOverflow.clip,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
final pdfBytes = await pdf.save();
|
|
|
|
// Impression via Printing
|
|
await Printing.layoutPdf(
|
|
onLayout: (format) async => pdfBytes,
|
|
name: 'QR_${reference ?? DateTime.now().millisecondsSinceEpoch}',
|
|
format: labelSize.pageFormat,
|
|
);
|
|
|
|
Get.snackbar(
|
|
'Succès',
|
|
'QR imprimé (${labelSize.name}) avec référence visible',
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
} catch (e) {
|
|
print('Erreur impression Niimbot: $e');
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Impression échouée: ${e.toString()}',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/// Méthode de calibrage pour trouver le décalage optimal
|
|
Future<void> printNiimbotCalibration({
|
|
NiimbotLabelSize labelSize = NiimbotLabelSize.small,
|
|
}) async {
|
|
try {
|
|
final pdf = pw.Document();
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: labelSize.pageFormat,
|
|
build: (context) => pw.Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border.all(width: 0.5, color: PdfColors.black),
|
|
),
|
|
child: pw.Stack(
|
|
children: [
|
|
// Grille de référence
|
|
pw.Positioned(
|
|
left: 0,
|
|
top: 0,
|
|
child: pw.Container(
|
|
width: labelSize.width * PdfPageFormat.mm,
|
|
height: labelSize.height * PdfPageFormat.mm,
|
|
child: pw.CustomPaint(
|
|
painter: (PdfGraphics canvas, PdfPoint size) {
|
|
// Lignes verticales tous les 5mm
|
|
for (int i = 5; i < labelSize.width; i += 5) {
|
|
canvas.drawLine(
|
|
i * PdfPageFormat.mm,
|
|
0,
|
|
i * PdfPageFormat.mm,
|
|
labelSize.height * PdfPageFormat.mm,
|
|
);
|
|
}
|
|
// Lignes horizontales tous les 5mm
|
|
for (int i = 5; i < labelSize.height; i += 5) {
|
|
canvas.drawLine(
|
|
0,
|
|
i * PdfPageFormat.mm,
|
|
labelSize.width * PdfPageFormat.mm,
|
|
i * PdfPageFormat.mm,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
// Marqueur centre
|
|
pw.Positioned(
|
|
left: (labelSize.width / 2 - 3) * PdfPageFormat.mm,
|
|
top: (labelSize.height / 2 - 3) * PdfPageFormat.mm,
|
|
child: pw.Container(
|
|
width: 6 * PdfPageFormat.mm,
|
|
height: 6 * PdfPageFormat.mm,
|
|
color: PdfColors.black,
|
|
child: pw.Center(
|
|
child: pw.Text(
|
|
'C',
|
|
style: pw.TextStyle(
|
|
color: PdfColors.white,
|
|
fontSize: 5,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Coin haut-gauche
|
|
pw.Positioned(
|
|
left: 1 * PdfPageFormat.mm,
|
|
top: 1 * PdfPageFormat.mm,
|
|
child: pw.Container(
|
|
width: 4 * PdfPageFormat.mm,
|
|
height: 4 * PdfPageFormat.mm,
|
|
color: PdfColors.black,
|
|
child: pw.Center(
|
|
child: pw.Text(
|
|
'1',
|
|
style: pw.TextStyle(
|
|
color: PdfColors.white,
|
|
fontSize: 3,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Titre
|
|
pw.Positioned(
|
|
left: 1 * PdfPageFormat.mm,
|
|
bottom: 1 * PdfPageFormat.mm,
|
|
child: pw.Text(
|
|
'CALIBRAGE ${labelSize.name}',
|
|
style: _getTextStyle(fontSize: 3, bold: true),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final pdfBytes = await pdf.save();
|
|
|
|
await Printing.layoutPdf(
|
|
onLayout: (format) async => pdfBytes,
|
|
name: 'Calibrage_Niimbot_${DateTime.now().millisecondsSinceEpoch}',
|
|
format: labelSize.pageFormat,
|
|
usePrinterSettings: false,
|
|
);
|
|
|
|
Get.snackbar(
|
|
'Calibrage',
|
|
'Page de calibrage envoyée (${labelSize.name})',
|
|
backgroundColor: Colors.blue,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 4),
|
|
);
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Erreur calibrage',
|
|
'Impossible d\'imprimer: $e',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Méthode avec paramètres ajustables pour corriger le décalage
|
|
Future<void> printQrNiimbotWithOffset(
|
|
String data, {
|
|
String? productName,
|
|
String? reference,
|
|
double offsetX = 0.0,
|
|
double offsetY = 0.0,
|
|
double scale = 1.0,
|
|
NiimbotLabelSize labelSize = NiimbotLabelSize.small,
|
|
}) async {
|
|
try {
|
|
await loadFonts();
|
|
|
|
final qrImageData = await _generateQrImage(data, size: 120);
|
|
final qrImage = pw.MemoryImage(qrImageData);
|
|
|
|
final pdf = pw.Document();
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: labelSize.pageFormat,
|
|
build: (context) => pw.Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
child: pw.Stack(
|
|
children: [
|
|
pw.Positioned(
|
|
left: (labelSize.width / 2 + offsetX) * PdfPageFormat.mm - (15 * scale) * PdfPageFormat.mm / 2,
|
|
top: (labelSize.height / 2 + offsetY) * PdfPageFormat.mm - (15 * scale) * PdfPageFormat.mm / 2,
|
|
child: pw.Transform(
|
|
transform: Matrix4.identity()..scale(scale),
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
pw.Container(
|
|
width: 12 * PdfPageFormat.mm,
|
|
height: 12 * PdfPageFormat.mm,
|
|
child: pw.Image(
|
|
qrImage,
|
|
fit: pw.BoxFit.contain,
|
|
),
|
|
),
|
|
|
|
if (reference != null && reference.isNotEmpty) ...[
|
|
pw.SizedBox(height: 0.5 * PdfPageFormat.mm),
|
|
pw.Container(
|
|
width: 20 * PdfPageFormat.mm,
|
|
child: pw.Text(
|
|
reference.length > 12 ? '${reference.substring(0, 12)}...' : reference,
|
|
style: _getTextStyle(fontSize: 4, bold: true),
|
|
textAlign: pw.TextAlign.center,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final pdfBytes = await pdf.save();
|
|
|
|
await Printing.layoutPdf(
|
|
onLayout: (format) async => pdfBytes,
|
|
name: 'QR_Offset_X${offsetX}_Y${offsetY}_S${scale}',
|
|
format: labelSize.pageFormat,
|
|
usePrinterSettings: false,
|
|
);
|
|
|
|
Get.snackbar(
|
|
'Test décalage',
|
|
'QR imprimé avec décalage X:${offsetX}mm Y:${offsetY}mm Scale:${scale}',
|
|
backgroundColor: Colors.orange,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Erreur test',
|
|
'Impossible d\'imprimer: $e',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Widget de dialogue pour sélectionner le format d'étiquette
|
|
void showNiimbotFormatDialog(Function(NiimbotLabelSize) onFormatSelected) {
|
|
Get.dialog(
|
|
AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Icon(Icons.straighten, color: Colors.orange[700]),
|
|
const SizedBox(width: 8),
|
|
const Text('Taille d\'étiquette'),
|
|
],
|
|
),
|
|
content: Container(
|
|
width: 300,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'Sélectionnez la taille de vos étiquettes Niimbot B1 :',
|
|
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Option Petit
|
|
_buildFormatOption(
|
|
size: NiimbotLabelSize.small,
|
|
icon: Icons.label_outline,
|
|
color: Colors.green,
|
|
onTap: () {
|
|
Get.back();
|
|
onFormatSelected(NiimbotLabelSize.small);
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
// Option Moyen
|
|
_buildFormatOption(
|
|
size: NiimbotLabelSize.medium,
|
|
icon: Icons.label,
|
|
color: Colors.orange,
|
|
onTap: () {
|
|
Get.back();
|
|
onFormatSelected(NiimbotLabelSize.medium);
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
// Option Grand
|
|
_buildFormatOption(
|
|
size: NiimbotLabelSize.large,
|
|
icon: Icons.label_important,
|
|
color: Colors.red,
|
|
onTap: () {
|
|
Get.back();
|
|
onFormatSelected(NiimbotLabelSize.large);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: const Text('Annuler'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFormatOption({
|
|
required NiimbotLabelSize size,
|
|
required IconData icon,
|
|
required Color color,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: color.withOpacity(0.5)),
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: color.withOpacity(0.1),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, color: color, size: 32),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
size.name,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: color.withOpacity(0.7),
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
Text(
|
|
'${size.width.toInt()}mm x ${size.height.toInt()}mm',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.arrow_forward_ios, size: 16, color: color),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Widget de dialogue pour ajuster les paramètres d'impression
|
|
void showNiimbotSettingsDialog() {
|
|
final offsetXController = TextEditingController(text: '0.0');
|
|
final offsetYController = TextEditingController(text: '0.0');
|
|
final scaleController = TextEditingController(text: '1.0');
|
|
final qrSizeController = TextEditingController(text: '12.0');
|
|
|
|
// Variable pour stocker la taille sélectionnée
|
|
Rx<NiimbotLabelSize> selectedSize = NiimbotLabelSize.small.obs;
|
|
|
|
Get.dialog(
|
|
Obx(() => AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Icon(Icons.settings, color: Colors.blue.shade700),
|
|
const SizedBox(width: 8),
|
|
const Text('Paramètres Niimbot B1'),
|
|
],
|
|
),
|
|
content: Container(
|
|
width: 400,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Sélection de format
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Format d\'étiquette :',
|
|
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orange.shade700),
|
|
),
|
|
const SizedBox(height: 8),
|
|
DropdownButton<NiimbotLabelSize>(
|
|
value: selectedSize.value,
|
|
isExpanded: true,
|
|
items: [
|
|
DropdownMenuItem(value: NiimbotLabelSize.small, child: Text(NiimbotLabelSize.small.name)),
|
|
DropdownMenuItem(value: NiimbotLabelSize.medium, child: Text(NiimbotLabelSize.medium.name)),
|
|
DropdownMenuItem(value: NiimbotLabelSize.large, child: Text(NiimbotLabelSize.large.name)),
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
selectedSize.value = value;
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Instructions
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Conseils pour corriger le décalage :',
|
|
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue.shade700),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text('• Décalage X : + = droite, - = gauche', style: TextStyle(fontSize: 12)),
|
|
Text('• Décalage Y : + = bas, - = haut', style: TextStyle(fontSize: 12)),
|
|
Text('• Échelle : 0.9 = plus petit, 1.1 = plus grand', style: TextStyle(fontSize: 12)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Champs de saisie
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: offsetXController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Décalage X (mm)',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: offsetYController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Décalage Y (mm)',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: scaleController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Échelle',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: qrSizeController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Taille QR (mm)',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: const Text('Fermer'),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
await printNiimbotCalibration(labelSize: selectedSize.value);
|
|
},
|
|
child: const Text('Calibrage'),
|
|
style: TextButton.styleFrom(foregroundColor: Colors.blue),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
final offsetX = double.tryParse(offsetXController.text) ?? 0.0;
|
|
final offsetY = double.tryParse(offsetYController.text) ?? 0.0;
|
|
final scale = double.tryParse(scaleController.text) ?? 1.0;
|
|
|
|
Get.back();
|
|
await printQrNiimbotWithOffset(
|
|
'https://stock.guycom.mg/TEST_${DateTime.now().millisecondsSinceEpoch}',
|
|
reference: 'TEST-${DateTime.now().millisecond}',
|
|
offsetX: offsetX,
|
|
offsetY: offsetY,
|
|
scale: scale,
|
|
labelSize: selectedSize.value,
|
|
);
|
|
},
|
|
child: const Text('Test'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.orange,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
)),
|
|
);
|
|
}
|
|
|
|
// ===== MÉTHODES ORIGINALES (CONSERVÉES POUR COMPATIBILITÉ) =====
|
|
|
|
/// Méthode spécialisée pour imprimantes Niimbot (ancienne version)
|
|
Future<void> printQrNiimbot(
|
|
String data, {
|
|
String? productName,
|
|
String? reference,
|
|
}) async {
|
|
try {
|
|
await loadFonts();
|
|
|
|
final qrImageData = await _generateQrImage(data, size: 200);
|
|
final qrImage = pw.MemoryImage(qrImageData);
|
|
|
|
final pdf = pw.Document();
|
|
|
|
// Format spécifique Niimbot : 50mm x 30mm
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: const PdfPageFormat(
|
|
50 * PdfPageFormat.mm,
|
|
30 * PdfPageFormat.mm,
|
|
marginAll: 0,
|
|
),
|
|
build: (context) => pw.Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
child: pw.Center(
|
|
child: pw.Column(
|
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
// QR Code centré
|
|
pw.Container(
|
|
width: 20 * PdfPageFormat.mm,
|
|
height: 20 * PdfPageFormat.mm,
|
|
child: pw.Image(
|
|
qrImage,
|
|
fit: pw.BoxFit.contain,
|
|
),
|
|
),
|
|
|
|
if (reference != null) ...[
|
|
pw.SizedBox(height: 2 * PdfPageFormat.mm),
|
|
pw.Text(
|
|
reference,
|
|
style: _getTextStyle(fontSize: 8, bold: true),
|
|
textAlign: pw.TextAlign.center,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final pdfBytes = await pdf.save();
|
|
|
|
await Printing.layoutPdf(
|
|
onLayout: (format) async => pdfBytes,
|
|
name: 'QR_Niimbot_${reference ?? 'etiquette'}',
|
|
format: const PdfPageFormat(
|
|
50 * PdfPageFormat.mm,
|
|
30 * PdfPageFormat.mm,
|
|
marginAll: 0,
|
|
),
|
|
usePrinterSettings: false,
|
|
);
|
|
|
|
Get.snackbar(
|
|
'Succès',
|
|
'QR Code Niimbot envoyé à l\'imprimante',
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
} catch (e) {
|
|
print('Erreur impression Niimbot: $e');
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Impossible d\'imprimer sur Niimbot: ${e.toString()}',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Prévisualise le PDF avant impression
|
|
Future<void> previewQr(
|
|
String data, {
|
|
String? productName,
|
|
String? reference,
|
|
bool isLabelFormat = true,
|
|
}) async {
|
|
try {
|
|
final pdfBytes = await generateQrPdf(
|
|
data,
|
|
productName: productName,
|
|
reference: reference,
|
|
isLabelFormat: isLabelFormat,
|
|
);
|
|
|
|
await Printing.layoutPdf(
|
|
onLayout: (format) async => pdfBytes,
|
|
name: 'Aperçu_QR_Code',
|
|
);
|
|
} catch (e) {
|
|
print('Erreur aperçu: $e');
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Impossible d\'afficher l\'aperçu: ${e.toString()}',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Sauvegarde le PDF
|
|
Future<void> saveQrPdf(
|
|
String data, {
|
|
String? productName,
|
|
String? reference,
|
|
bool isLabelFormat = true,
|
|
String? fileName,
|
|
}) async {
|
|
try {
|
|
final pdfBytes = await generateQrPdf(
|
|
data,
|
|
productName: productName,
|
|
reference: reference,
|
|
isLabelFormat: isLabelFormat,
|
|
);
|
|
|
|
final directory = await getApplicationDocumentsDirectory();
|
|
final file = File('${directory.path}/${fileName ?? 'qr_code_${reference ?? DateTime.now().millisecondsSinceEpoch}'}.pdf');
|
|
|
|
await file.writeAsBytes(pdfBytes);
|
|
|
|
Get.snackbar(
|
|
'Succès',
|
|
'PDF sauvegardé: ${file.path}',
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
} catch (e) {
|
|
print('Erreur sauvegarde: $e');
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Impossible de sauvegarder: ${e.toString()}',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
}
|