first commit
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// import '../../../../data/repository/repository.dart';
|
||||
|
||||
// export '../../../../data/repository/repository.dart' show SalePurchaseThermalInvoiceData;
|
||||
|
||||
// final purchaseThermalInvoiceProvider = FutureProvider.autoDispose.family<bool, SalePurchaseThermalInvoiceData>(
|
||||
// (ref, purchase) async {
|
||||
// final _user = ref.read(userRepositoryProvider).value;
|
||||
|
||||
// final _template = PurchaseThermalInvoiceTemplate(
|
||||
// ref,
|
||||
// purchaseInvoice: purchase.copyWith(user: _user),
|
||||
// );
|
||||
|
||||
// return await Future.microtask(
|
||||
// () => ref.read(thermalPrinterGeneratorProvider).printInvoice(_template),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// final saleThermalInvoiceProvider =
|
||||
// FutureProvider.autoDispose.family<bool, ({SalePurchaseThermalInvoiceData sale, bool printKOT})>(
|
||||
// (ref, data) async {
|
||||
// final _user = ref.read(userRepositoryProvider).value;
|
||||
|
||||
// final _template = SaleThermalInvoiceTemplate(
|
||||
// ref,
|
||||
// saleInvoice: data.sale.copyWith(user: _user),
|
||||
// printKOT: data.printKOT,
|
||||
// );
|
||||
|
||||
// return await Future.microtask(
|
||||
// () => ref.read(thermalPrinterGeneratorProvider).printInvoice(_template),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// final kotThermalInvoiceProvider = FutureProvider.autoDispose.family<bool, Sale>(
|
||||
// (ref, sale) async {
|
||||
// final _user = ref.read(userRepositoryProvider).value;
|
||||
|
||||
// final _template = KOTTicketTemplate(
|
||||
// ref,
|
||||
// kotInvoice: SalePurchaseThermalInvoiceData.fromSale(sale).copyWith(user: _user),
|
||||
// );
|
||||
|
||||
// return await Future.microtask(
|
||||
// () => ref.read(thermalPrinterGeneratorProvider).printInvoice(_template),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// final dueCollectionThermalInvoiceProvider = FutureProvider.autoDispose.family<bool, DueCollectionThermalInvoiceData>(
|
||||
// (ref, dueCollect) async {
|
||||
// final _user = ref.read(userRepositoryProvider).value;
|
||||
|
||||
// final _template = DueCollectionTemplate(
|
||||
// ref,
|
||||
// dueInvoice: dueCollect.copyWith(user: _user),
|
||||
// );
|
||||
|
||||
// return await Future.microtask(
|
||||
// () => ref.read(thermalPrinterGeneratorProvider).printInvoice(_template),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// sealed class InvoicePreviewType {
|
||||
// const InvoicePreviewType._();
|
||||
// }
|
||||
|
||||
// class ThermalPreview extends InvoicePreviewType {
|
||||
// final ThermalPrintInvoiceData printData;
|
||||
// final bool isSale;
|
||||
// ThermalPreview(this.printData, {this.isSale = false}) : super._();
|
||||
// }
|
||||
|
||||
// class PdfPreview extends InvoicePreviewType {
|
||||
// PdfPreview() : super._();
|
||||
// }
|
||||
322
lib/service/thermal_print/thermer/src/layouts/_layout_utils.dart
Normal file
322
lib/service/thermal_print/thermer/src/layouts/_layout_utils.dart
Normal file
@@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/widgets.export.dart';
|
||||
|
||||
class TextMeasurementCache {
|
||||
static final Map<String, _CachedMeasurement> _cache = {};
|
||||
|
||||
static void clear() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
static _CachedMeasurement _getCachedMeasurement(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth,
|
||||
List<String>? fallbackFonts,
|
||||
) {
|
||||
final key = _generateKey(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
);
|
||||
if (_cache.length >= 1000) {
|
||||
_cache.clear();
|
||||
}
|
||||
return _cache[key] ??= _CachedMeasurement(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
);
|
||||
}
|
||||
|
||||
static String _generateKey(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth,
|
||||
List<String>? fallbackFonts,
|
||||
) {
|
||||
return '$text|${style.hashCode}|$direction|$textAlign|$maxLines|$maxWidth|${fallbackFonts?.join(',')}';
|
||||
}
|
||||
|
||||
static double getWidth(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth, [
|
||||
List<String>? fallbackFonts,
|
||||
]) {
|
||||
return _getCachedMeasurement(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
).width;
|
||||
}
|
||||
|
||||
static double getHeight(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth, [
|
||||
List<String>? fallbackFonts,
|
||||
]) {
|
||||
return _getCachedMeasurement(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
).height;
|
||||
}
|
||||
|
||||
static TextPainter getPainter(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth, [
|
||||
List<String>? fallbackFonts,
|
||||
]) {
|
||||
return _getCachedMeasurement(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
).painter;
|
||||
}
|
||||
}
|
||||
|
||||
class _CachedMeasurement {
|
||||
late final TextPainter painter;
|
||||
late final double width;
|
||||
late final double height;
|
||||
|
||||
_CachedMeasurement(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth,
|
||||
List<String>? fallbackFonts,
|
||||
) {
|
||||
final effectiveStyle =
|
||||
fallbackFonts != null && fallbackFonts.isNotEmpty ? style.copyWith(fontFamilyFallback: fallbackFonts) : style;
|
||||
if (fallbackFonts != null && fallbackFonts.isNotEmpty) {
|
||||
debugPrint('TextMeasurementCache: Using font fallback for text: "$text"');
|
||||
}
|
||||
painter = TextPainter(
|
||||
text: TextSpan(text: text, style: effectiveStyle),
|
||||
textDirection: direction,
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
);
|
||||
painter.layout(maxWidth: maxWidth);
|
||||
width = painter.width;
|
||||
height = painter.height;
|
||||
}
|
||||
}
|
||||
|
||||
class LayoutUtils {
|
||||
static List<double> calculateCellWidths(
|
||||
ThermerTable table,
|
||||
double availableWidth,
|
||||
int numColumns,
|
||||
) {
|
||||
List<double> actualWidths = [];
|
||||
if (table.cellWidths != null) {
|
||||
double totalFixed = 0;
|
||||
int nullCount = 0;
|
||||
for (int col = 0; col < numColumns; col++) {
|
||||
double? frac = table.cellWidths![col];
|
||||
if (frac != null) {
|
||||
totalFixed += frac;
|
||||
} else {
|
||||
nullCount++;
|
||||
}
|
||||
}
|
||||
double remaining = 1.0 - totalFixed;
|
||||
double nullFrac = nullCount > 0 ? remaining / nullCount : 0;
|
||||
for (int col = 0; col < numColumns; col++) {
|
||||
double? frac = table.cellWidths![col];
|
||||
actualWidths.add((frac ?? nullFrac) * availableWidth);
|
||||
}
|
||||
} else {
|
||||
double totalSpacing = (numColumns - 1) * table.columnSpacing;
|
||||
double cellWidth = (availableWidth - totalSpacing) / numColumns;
|
||||
actualWidths = List.filled(numColumns, cellWidth);
|
||||
}
|
||||
return actualWidths;
|
||||
}
|
||||
|
||||
static double calculateWidgetWidth(
|
||||
ThermerWidget widget,
|
||||
double availableWidth, {
|
||||
TextDirection defaultTextDirection = TextDirection.ltr,
|
||||
}) {
|
||||
if (widget is ThermerText) {
|
||||
return TextMeasurementCache.getWidth(
|
||||
widget.data,
|
||||
widget.style ?? const TextStyle(),
|
||||
widget.direction ?? defaultTextDirection,
|
||||
widget.textAlign,
|
||||
widget.maxLines,
|
||||
availableWidth,
|
||||
widget.fallbackFonts,
|
||||
);
|
||||
} else if (widget is ThermerSizedBox) {
|
||||
return widget.width ?? availableWidth;
|
||||
} else if (widget is ThermerQRCode) {
|
||||
return widget.size;
|
||||
} else if (widget is ThermerImage) {
|
||||
return widget.width ?? availableWidth;
|
||||
} else if (widget is ThermerExpanded) {
|
||||
return calculateWidgetWidth(widget.child, availableWidth, defaultTextDirection: defaultTextDirection);
|
||||
} else if (widget is ThermerFlexible) {
|
||||
return calculateWidgetWidth(widget.child, availableWidth, defaultTextDirection: defaultTextDirection);
|
||||
} else if (widget is ThermerAlign) {
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
static double calculateWidgetHeight(
|
||||
ThermerWidget widget,
|
||||
double maxWidth, {
|
||||
TextDirection defaultTextDirection = TextDirection.ltr,
|
||||
}) {
|
||||
if (widget is ThermerText) {
|
||||
return TextMeasurementCache.getHeight(
|
||||
widget.data,
|
||||
widget.style ?? const TextStyle(),
|
||||
widget.direction ?? defaultTextDirection,
|
||||
widget.textAlign,
|
||||
widget.maxLines,
|
||||
maxWidth,
|
||||
widget.fallbackFonts,
|
||||
);
|
||||
} else if (widget is ThermerRow) {
|
||||
double maxHeight = 0;
|
||||
for (final child in widget.children) {
|
||||
final childHeight = calculateWidgetHeight(child, maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
if (childHeight > maxHeight) maxHeight = childHeight;
|
||||
}
|
||||
return maxHeight;
|
||||
} else if (widget is ThermerColumn) {
|
||||
double totalHeight = 0;
|
||||
for (int i = 0; i < widget.children.length; i++) {
|
||||
totalHeight += calculateWidgetHeight(widget.children[i], maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
if (i < widget.children.length - 1) totalHeight += widget.spacing;
|
||||
}
|
||||
return totalHeight;
|
||||
} else if (widget is ThermerTable) {
|
||||
final numColumns = widget.data.isNotEmpty ? widget.data[0].cells.length : (widget.header?.cells.length ?? 0);
|
||||
final actualWidths = calculateCellWidths(widget, maxWidth, numColumns);
|
||||
|
||||
double totalHeight = 0;
|
||||
|
||||
double borderHeight = 0;
|
||||
if (widget.enableHeaderBorders && widget.header != null) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: widget.horizontalBorderChar,
|
||||
style: TextStyle(fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout(maxWidth: maxWidth);
|
||||
borderHeight = textPainter.height;
|
||||
}
|
||||
|
||||
if (widget.header != null) {
|
||||
if (widget.enableHeaderBorders) {
|
||||
totalHeight += borderHeight;
|
||||
}
|
||||
double rowHeight = 0;
|
||||
for (int col = 0; col < widget.header!.cells.length; col++) {
|
||||
final cell = widget.header!.cells[col];
|
||||
final cellWidth = actualWidths[col];
|
||||
final cellHeight = calculateWidgetHeight(cell, cellWidth, defaultTextDirection: defaultTextDirection);
|
||||
if (cellHeight > rowHeight) rowHeight = cellHeight;
|
||||
}
|
||||
totalHeight += rowHeight;
|
||||
if (widget.enableHeaderBorders) {
|
||||
totalHeight += widget.rowSpacing + borderHeight;
|
||||
} else {
|
||||
totalHeight += widget.rowSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < widget.data.length; i++) {
|
||||
double rowHeight = 0;
|
||||
for (int col = 0; col < widget.data[i].cells.length; col++) {
|
||||
final cell = widget.data[i].cells[col];
|
||||
final cellWidth = actualWidths[col];
|
||||
final cellHeight = calculateWidgetHeight(cell, cellWidth, defaultTextDirection: defaultTextDirection);
|
||||
if (cellHeight > rowHeight) rowHeight = cellHeight;
|
||||
}
|
||||
totalHeight += rowHeight;
|
||||
if (i < widget.data.length - 1) totalHeight += widget.rowSpacing;
|
||||
}
|
||||
return totalHeight;
|
||||
} else if (widget is ThermerDivider) {
|
||||
if (widget.isHorizontal) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: widget.character,
|
||||
style: TextStyle(fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout(maxWidth: maxWidth);
|
||||
return textPainter.height;
|
||||
} else {
|
||||
return (widget.length ?? 1) * 20.0;
|
||||
}
|
||||
} else if (widget is ThermerQRCode) {
|
||||
return widget.size;
|
||||
} else if (widget is ThermerImage) {
|
||||
return widget.height ?? ((widget.width ?? maxWidth) / widget.image.width) * widget.image.height;
|
||||
} else if (widget is ThermerSizedBox) {
|
||||
return widget.height ??
|
||||
(widget.child != null
|
||||
? calculateWidgetHeight(widget.child!, maxWidth, defaultTextDirection: defaultTextDirection)
|
||||
: 0);
|
||||
} else if (widget is ThermerExpanded) {
|
||||
return calculateWidgetHeight(widget.child, maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
} else if (widget is ThermerFlexible) {
|
||||
return calculateWidgetHeight(widget.child, maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
} else if (widget is ThermerAlign) {
|
||||
return calculateWidgetHeight(widget.child, maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import '../widgets/widgets.export.dart';
|
||||
|
||||
class LayoutItem {
|
||||
final ThermerWidget widget;
|
||||
final double height;
|
||||
|
||||
const LayoutItem({
|
||||
required this.widget,
|
||||
required this.height,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'dart:typed_data' as type;
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import '_shared_types.dart';
|
||||
import '_thermer_painter.dart';
|
||||
import '_layout_utils.dart';
|
||||
import '../widgets/widgets.export.dart';
|
||||
|
||||
class PaperSize {
|
||||
const PaperSize._(this._width);
|
||||
|
||||
static const mm58 = PaperSize._(58.0);
|
||||
static const mm80 = PaperSize._(80.0);
|
||||
static const mm110 = PaperSize._(110.0);
|
||||
|
||||
const PaperSize.custom(double width) : _width = width;
|
||||
|
||||
final double _width;
|
||||
|
||||
double get width => _width;
|
||||
}
|
||||
|
||||
/// Main class for creating thermal printer layouts from widgets.
|
||||
/// Handles layout calculation, rendering to image, and conversion to byte data.
|
||||
class ThermerLayout {
|
||||
/// The list of widgets to include in the layout.
|
||||
final List<ThermerWidget> widgets;
|
||||
|
||||
/// The paper size for the thermal printer.
|
||||
final PaperSize paperSize;
|
||||
|
||||
/// Dots per inch for the printer resolution.
|
||||
final double dpi;
|
||||
|
||||
/// Gap between layout items.
|
||||
final double layoutGap;
|
||||
|
||||
/// Whether to convert the output to black and white.
|
||||
final bool blackAndWhite;
|
||||
|
||||
/// Horizontal margin in millimeters to account for printer limitations.
|
||||
final double marginMm;
|
||||
|
||||
/// The default text direction for the layout.
|
||||
final TextDirection textDirection;
|
||||
|
||||
const ThermerLayout({
|
||||
required this.widgets,
|
||||
this.paperSize = PaperSize.mm80,
|
||||
double? dpi,
|
||||
this.layoutGap = 3.0,
|
||||
this.blackAndWhite = false,
|
||||
double? marginMm,
|
||||
this.textDirection = TextDirection.ltr,
|
||||
}) : dpi = dpi ?? 203.0,
|
||||
marginMm = marginMm ?? 5.0;
|
||||
|
||||
double get width => ((paperSize.width - (marginMm * 2)) / 25.4) * dpi;
|
||||
|
||||
// Process layout and calculate heights in one pass
|
||||
List<LayoutItem> _processLayout() {
|
||||
return widgets.map((widget) {
|
||||
final height = _calculateHeight(widget);
|
||||
return LayoutItem(widget: widget, height: height);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
double _calculateHeight(ThermerWidget widget) {
|
||||
// subtract margins from width to get printable area
|
||||
final printableWidth = width;
|
||||
final height = LayoutUtils.calculateWidgetHeight(widget, printableWidth, defaultTextDirection: textDirection);
|
||||
if (height == 0 && widget is ThermerText) {
|
||||
throw Exception('ThermerText height is 0 for text: "${widget.data}"');
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
double _calculateTotalHeight(List<LayoutItem> items) {
|
||||
double total = 0;
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
total += items[i].height;
|
||||
if (i < items.length - 1) total += layoutGap;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
Future<type.Uint8List> toUint8List() => generateImage();
|
||||
|
||||
Future<type.Uint8List> generateImage() async {
|
||||
TextMeasurementCache.clear();
|
||||
|
||||
if (widgets.isEmpty) {
|
||||
throw Exception('No widgets provided to ThermerLayout');
|
||||
}
|
||||
|
||||
final layoutItems = _processLayout();
|
||||
final totalHeight = _calculateTotalHeight(layoutItems);
|
||||
|
||||
if (totalHeight <= 1) {
|
||||
throw Exception('Total height is $totalHeight, cannot generate image');
|
||||
}
|
||||
|
||||
if (width <= 0 || width > 10000) {
|
||||
throw Exception('Invalid width: $width. Must be > 0 and <= 10000');
|
||||
}
|
||||
|
||||
if (totalHeight <= 0 || totalHeight > 10000) {
|
||||
throw Exception('Invalid height: $totalHeight. Must be > 0 and <= 10000');
|
||||
}
|
||||
|
||||
debugPrint('ThermerLayout: Generating image with size ${width.toInt()}x${totalHeight.toInt()}');
|
||||
|
||||
final size = ui.Size(width, totalHeight);
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = ui.Canvas(recorder);
|
||||
|
||||
// Draw white background
|
||||
final paint = Paint()..color = const Color(0xFFFFFFFF);
|
||||
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
|
||||
|
||||
final painter = ThermerPainter(layoutItems, layoutGap: layoutGap, textDirection: textDirection);
|
||||
painter.paint(canvas, size);
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(
|
||||
size.width.toInt(),
|
||||
size.height.toInt(),
|
||||
);
|
||||
|
||||
var byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
var bytes = byteData!.buffer.asUint8List();
|
||||
|
||||
if (blackAndWhite) {
|
||||
debugPrint('ThermerLayout: Converting image to black and white');
|
||||
// Decode the PNG
|
||||
final decodedImage = img.decodePng(bytes);
|
||||
if (decodedImage != null) {
|
||||
// Convert to monochrome (1-bit)
|
||||
final monoImage = img.monochrome(decodedImage);
|
||||
// Encode back to PNG
|
||||
bytes = img.encodePng(monoImage);
|
||||
debugPrint('ThermerLayout: B&W conversion completed');
|
||||
} else {
|
||||
debugPrint('ThermerLayout: Failed to decode image for B&W conversion');
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import '_shared_types.dart';
|
||||
import '_layout_utils.dart';
|
||||
import '../widgets/widgets.export.dart';
|
||||
|
||||
class ThermerPainter extends CustomPainter {
|
||||
final List<LayoutItem> layoutItems;
|
||||
final double layoutGap;
|
||||
final TextDirection textDirection;
|
||||
static const double charWidth = 10;
|
||||
|
||||
ThermerPainter(this.layoutItems,
|
||||
{this.layoutGap = 3.0, this.textDirection = TextDirection.ltr});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
double yOffset = 0;
|
||||
final linePaint = Paint()
|
||||
..color = const Color(0xFF000000)
|
||||
..strokeWidth = 1;
|
||||
|
||||
for (final item in layoutItems) {
|
||||
_paintWidget(canvas, size, item.widget, yOffset, linePaint);
|
||||
yOffset += item.height + layoutGap;
|
||||
}
|
||||
}
|
||||
|
||||
void _paintWidget(Canvas canvas, Size size, ThermerWidget widget,
|
||||
double yOffset, Paint linePaint) {
|
||||
if (widget is ThermerText) {
|
||||
final textPainter = TextMeasurementCache.getPainter(
|
||||
widget.data,
|
||||
widget.style ?? const TextStyle(),
|
||||
widget.direction ?? textDirection,
|
||||
widget.textAlign,
|
||||
widget.maxLines,
|
||||
size.width,
|
||||
widget.fallbackFonts,
|
||||
);
|
||||
double xOffset = 0;
|
||||
|
||||
final effectiveAlign = widget.textAlign;
|
||||
|
||||
if (effectiveAlign == TextAlign.center) {
|
||||
xOffset = (size.width - textPainter.width) / 2;
|
||||
} else if (effectiveAlign == TextAlign.right) {
|
||||
xOffset = size.width - textPainter.width;
|
||||
} else if (effectiveAlign == TextAlign.left) {
|
||||
xOffset = 0;
|
||||
} else if (effectiveAlign == TextAlign.start) {
|
||||
xOffset = textDirection == TextDirection.rtl
|
||||
? size.width - textPainter.width
|
||||
: 0;
|
||||
} else if (effectiveAlign == TextAlign.end) {
|
||||
xOffset = textDirection == TextDirection.rtl
|
||||
? 0
|
||||
: size.width - textPainter.width;
|
||||
} else if (effectiveAlign == TextAlign.justify) {
|
||||
xOffset = textDirection == TextDirection.rtl
|
||||
? size.width - textPainter.width
|
||||
: 0;
|
||||
}
|
||||
|
||||
textPainter.paint(canvas, Offset(xOffset, yOffset));
|
||||
} else if (widget is ThermerRow) {
|
||||
_paintRow(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerColumn) {
|
||||
_paintColumn(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerTable) {
|
||||
_paintTable(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerDivider) {
|
||||
_paintDivider(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerImage) {
|
||||
_paintImage(canvas, size, widget, yOffset);
|
||||
} else if (widget is ThermerQRCode) {
|
||||
_paintQRCode(canvas, size, widget, yOffset);
|
||||
} else if (widget is ThermerAlign) {
|
||||
_paintAlign(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerExpanded) {
|
||||
_paintWidget(canvas, size, widget.child, yOffset, linePaint);
|
||||
} else if (widget is ThermerFlexible) {
|
||||
_paintWidget(canvas, size, widget.child, yOffset, linePaint);
|
||||
} else if (widget is ThermerSizedBox) {
|
||||
if (widget.child != null) {
|
||||
final childSize = Size(
|
||||
widget.width ?? size.width,
|
||||
widget.height ?? _calculateChildHeight(widget.child!, size.width),
|
||||
);
|
||||
_paintWidget(canvas, childSize, widget.child!, yOffset, linePaint);
|
||||
}
|
||||
} else {
|
||||
throw Exception('Unknown widget type: ${widget.runtimeType}');
|
||||
}
|
||||
}
|
||||
|
||||
void _paintTable(Canvas canvas, Size size, ThermerTable widget,
|
||||
double yOffset, Paint linePaint) {
|
||||
final numColumns = widget.data.isNotEmpty
|
||||
? widget.data[0].cells.length
|
||||
: (widget.header?.cells.length ?? 0);
|
||||
final actualWidths =
|
||||
LayoutUtils.calculateCellWidths(widget, size.width, numColumns);
|
||||
|
||||
double currentY = yOffset;
|
||||
final isRtl = textDirection == TextDirection.rtl;
|
||||
|
||||
double getColumnX(int colIndex) {
|
||||
if (!isRtl) {
|
||||
double x = 0;
|
||||
for (int i = 0; i < colIndex; i++) {
|
||||
x += actualWidths[i];
|
||||
if (widget.cellWidths == null) x += widget.columnSpacing;
|
||||
}
|
||||
return x;
|
||||
} else {
|
||||
double rightEdgeOffset = 0;
|
||||
for (int i = 0; i < colIndex; i++) {
|
||||
rightEdgeOffset += actualWidths[i];
|
||||
if (widget.cellWidths == null)
|
||||
rightEdgeOffset += widget.columnSpacing;
|
||||
}
|
||||
return size.width - rightEdgeOffset - actualWidths[colIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.header != null) {
|
||||
if (widget.enableHeaderBorders) {
|
||||
_paintHorizontalBorder(
|
||||
canvas, size, currentY, widget.horizontalBorderChar);
|
||||
currentY += _getBorderHeight(size.width, widget.horizontalBorderChar);
|
||||
}
|
||||
|
||||
double rowHeight = 0;
|
||||
for (int col = 0; col < widget.header!.cells.length; col++) {
|
||||
final cellWidget = widget.header!.cells[col];
|
||||
final cellWidth = actualWidths[col];
|
||||
final cellSize = Size(cellWidth, double.infinity);
|
||||
|
||||
final x = getColumnX(col);
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(x, 0);
|
||||
_paintWidget(canvas, cellSize, cellWidget, currentY, linePaint);
|
||||
canvas.restore();
|
||||
|
||||
final cellHeight = _calculateChildHeight(cellWidget, cellWidth);
|
||||
if (cellHeight > rowHeight) rowHeight = cellHeight;
|
||||
}
|
||||
currentY += rowHeight;
|
||||
|
||||
if (widget.enableHeaderBorders) {
|
||||
currentY += widget.rowSpacing;
|
||||
_paintHorizontalBorder(
|
||||
canvas, size, currentY, widget.horizontalBorderChar);
|
||||
currentY += _getBorderHeight(size.width, widget.horizontalBorderChar);
|
||||
}
|
||||
|
||||
currentY += widget.rowSpacing;
|
||||
}
|
||||
|
||||
for (int i = 0; i < widget.data.length; i++) {
|
||||
final row = widget.data[i];
|
||||
double rowHeight = 0;
|
||||
|
||||
for (int col = 0; col < row.cells.length; col++) {
|
||||
final cellWidget = row.cells[col];
|
||||
final cellWidth = actualWidths[col];
|
||||
final cellSize = Size(cellWidth, double.infinity);
|
||||
|
||||
final x = getColumnX(col);
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(x, 0);
|
||||
_paintWidget(canvas, cellSize, cellWidget, currentY, linePaint);
|
||||
canvas.restore();
|
||||
|
||||
final cellHeight = _calculateChildHeight(cellWidget, cellWidth);
|
||||
if (cellHeight > rowHeight) rowHeight = cellHeight;
|
||||
}
|
||||
currentY += rowHeight;
|
||||
if (i < widget.data.length - 1) currentY += widget.rowSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
void _paintHorizontalBorder(
|
||||
Canvas canvas, Size size, double yOffset, String char) {
|
||||
const defaultStyle = TextStyle(
|
||||
fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500);
|
||||
final borderLength =
|
||||
_calculateDividerLength(char, size.width, defaultStyle);
|
||||
final borderPainter = TextMeasurementCache.getPainter(
|
||||
char * borderLength,
|
||||
defaultStyle,
|
||||
TextDirection.ltr,
|
||||
TextAlign.left,
|
||||
null,
|
||||
size.width,
|
||||
);
|
||||
borderPainter.paint(canvas, Offset(0, yOffset));
|
||||
}
|
||||
|
||||
double _getBorderHeight(double width, String char) {
|
||||
const defaultStyle = TextStyle(
|
||||
fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500);
|
||||
final borderLength = _calculateDividerLength(char, width, defaultStyle);
|
||||
final borderPainter = TextMeasurementCache.getPainter(
|
||||
char * borderLength,
|
||||
defaultStyle,
|
||||
TextDirection.ltr,
|
||||
TextAlign.left,
|
||||
null,
|
||||
width,
|
||||
);
|
||||
return borderPainter.height;
|
||||
}
|
||||
|
||||
void _paintRow(Canvas canvas, Size size, ThermerRow row, double yOffset,
|
||||
Paint linePaint) {
|
||||
if (row.children.isEmpty) return;
|
||||
|
||||
final fixedChildren = <ThermerWidget>[];
|
||||
final flexibleChildren = <ThermerWidget>[];
|
||||
final fixedIndices = <int>[];
|
||||
final flexibleIndices = <int>[];
|
||||
final flexValues = <int>[];
|
||||
|
||||
for (int i = 0; i < row.children.length; i++) {
|
||||
final child = row.children[i];
|
||||
if (child is ThermerExpanded) {
|
||||
flexibleChildren.add(child);
|
||||
flexibleIndices.add(i);
|
||||
flexValues.add(child.flex);
|
||||
} else if (child is ThermerFlexible &&
|
||||
child.fit == ThermerFlexFit.loose) {
|
||||
flexibleChildren.add(child);
|
||||
flexibleIndices.add(i);
|
||||
flexValues.add(child.flex);
|
||||
} else {
|
||||
fixedChildren.add(child);
|
||||
fixedIndices.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
final fixedWidths = fixedChildren
|
||||
.map((child) => _calculateChildWidth(child, size.width))
|
||||
.toList();
|
||||
|
||||
final childHeights = row.children
|
||||
.map((child) => _calculateChildHeight(
|
||||
child, _calculateChildWidth(child, size.width)))
|
||||
.toList();
|
||||
final rowHeight = childHeights.reduce((a, b) => a > b ? a : b);
|
||||
|
||||
final totalFixedWidth =
|
||||
fixedWidths.isNotEmpty ? fixedWidths.reduce((a, b) => a + b) : 0;
|
||||
final totalSpacing = (row.children.length - 1) * row.spacing;
|
||||
final remainingWidth = size.width - totalFixedWidth - totalSpacing;
|
||||
|
||||
final totalFlex =
|
||||
flexValues.isNotEmpty ? flexValues.reduce((a, b) => a + b) : 0;
|
||||
final flexibleWidths = <double>[];
|
||||
if (totalFlex > 0 && remainingWidth > 0) {
|
||||
for (final flex in flexValues) {
|
||||
flexibleWidths.add((flex / totalFlex) * remainingWidth);
|
||||
}
|
||||
} else {
|
||||
flexibleWidths.addAll(List.filled(flexibleChildren.length, 0.0));
|
||||
}
|
||||
|
||||
final actualWidths = List<double>.filled(row.children.length, 0);
|
||||
for (int i = 0; i < fixedIndices.length; i++) {
|
||||
actualWidths[fixedIndices[i]] = fixedWidths[i];
|
||||
}
|
||||
for (int i = 0; i < flexibleIndices.length; i++) {
|
||||
actualWidths[flexibleIndices[i]] = flexibleWidths[i];
|
||||
}
|
||||
|
||||
final totalChildrenWidth = actualWidths.reduce((a, b) => a + b);
|
||||
final isRtl = textDirection == TextDirection.rtl;
|
||||
|
||||
double startX = 0;
|
||||
double dynamicSpacing = row.spacing;
|
||||
|
||||
var effectiveAlignment = row.mainAxisAlignment;
|
||||
|
||||
switch (effectiveAlignment) {
|
||||
case ThermerMainAxisAlignment.start:
|
||||
startX = isRtl ? size.width : 0;
|
||||
break;
|
||||
case ThermerMainAxisAlignment.center:
|
||||
final offset = (size.width -
|
||||
totalChildrenWidth -
|
||||
(row.children.length - 1) * row.spacing) /
|
||||
2;
|
||||
startX = isRtl ? size.width - offset : offset;
|
||||
break;
|
||||
case ThermerMainAxisAlignment.end:
|
||||
final offset = size.width -
|
||||
totalChildrenWidth -
|
||||
(row.children.length - 1) * row.spacing;
|
||||
startX = isRtl ? size.width - offset : offset;
|
||||
|
||||
startX = isRtl
|
||||
? totalChildrenWidth + (row.children.length - 1) * row.spacing
|
||||
: size.width -
|
||||
totalChildrenWidth -
|
||||
(row.children.length - 1) * row.spacing;
|
||||
break;
|
||||
case ThermerMainAxisAlignment.spaceBetween:
|
||||
startX = isRtl ? size.width : 0;
|
||||
if (row.children.isNotEmpty && row.children.length > 1) {
|
||||
dynamicSpacing =
|
||||
(size.width - totalChildrenWidth) / (row.children.length - 1);
|
||||
}
|
||||
break;
|
||||
case ThermerMainAxisAlignment.spaceAround:
|
||||
final totalSpace = size.width - totalChildrenWidth;
|
||||
final spacePerChild =
|
||||
row.children.isNotEmpty ? totalSpace / row.children.length : 0.0;
|
||||
startX = isRtl ? size.width - spacePerChild / 2 : spacePerChild / 2;
|
||||
dynamicSpacing = spacePerChild;
|
||||
break;
|
||||
case ThermerMainAxisAlignment.spaceEvenly:
|
||||
final totalSpace = size.width - totalChildrenWidth;
|
||||
final spacePerGap = row.children.isNotEmpty
|
||||
? totalSpace / (row.children.length + 1)
|
||||
: 0.0;
|
||||
startX = isRtl ? size.width - spacePerGap : spacePerGap;
|
||||
dynamicSpacing = spacePerGap;
|
||||
break;
|
||||
}
|
||||
|
||||
double currentX = startX;
|
||||
|
||||
for (int i = 0; i < row.children.length; i++) {
|
||||
final child = row.children[i];
|
||||
final childWidth = actualWidths[i];
|
||||
final childHeight = childHeights[i];
|
||||
|
||||
double childY = yOffset;
|
||||
double effectiveChildHeight = rowHeight;
|
||||
if (row.crossAxisAlignment == ThermerCrossAxisAlignment.center) {
|
||||
childY += (rowHeight - childHeight) / 2;
|
||||
} else if (row.crossAxisAlignment == ThermerCrossAxisAlignment.end) {
|
||||
childY += rowHeight - childHeight;
|
||||
} else if (row.crossAxisAlignment == ThermerCrossAxisAlignment.stretch) {
|
||||
effectiveChildHeight = rowHeight;
|
||||
childY = yOffset;
|
||||
} else {
|
||||
effectiveChildHeight = childHeight;
|
||||
childY = yOffset;
|
||||
}
|
||||
|
||||
double paintX;
|
||||
if (isRtl) {
|
||||
currentX -= childWidth;
|
||||
paintX = currentX;
|
||||
} else {
|
||||
paintX = currentX;
|
||||
currentX += childWidth;
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(paintX, 0);
|
||||
_paintWidget(canvas, Size(childWidth, effectiveChildHeight), child,
|
||||
childY, linePaint);
|
||||
canvas.restore();
|
||||
|
||||
if (i < row.children.length - 1) {
|
||||
if (isRtl) {
|
||||
currentX -= dynamicSpacing;
|
||||
} else {
|
||||
currentX += dynamicSpacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintColumn(Canvas canvas, Size size, ThermerColumn column,
|
||||
double yOffset, Paint linePaint) {
|
||||
if (column.children.isEmpty) return;
|
||||
|
||||
final fixedChildren = <ThermerWidget>[];
|
||||
final flexibleChildren = <ThermerWidget>[];
|
||||
final fixedIndices = <int>[];
|
||||
final flexibleIndices = <int>[];
|
||||
final flexValues = <int>[];
|
||||
|
||||
for (int i = 0; i < column.children.length; i++) {
|
||||
final child = column.children[i];
|
||||
if (child is ThermerExpanded) {
|
||||
flexibleChildren.add(child);
|
||||
flexibleIndices.add(i);
|
||||
flexValues.add(child.flex);
|
||||
} else if (child is ThermerFlexible &&
|
||||
child.fit == ThermerFlexFit.loose) {
|
||||
flexibleChildren.add(child);
|
||||
flexibleIndices.add(i);
|
||||
flexValues.add(child.flex);
|
||||
} else {
|
||||
fixedChildren.add(child);
|
||||
fixedIndices.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
final fixedHeights = fixedChildren
|
||||
.map((child) => _calculateChildHeight(
|
||||
child, _calculateChildWidth(child, size.width)))
|
||||
.toList();
|
||||
final totalFixedHeight =
|
||||
fixedHeights.isNotEmpty ? fixedHeights.reduce((a, b) => a + b) : 0;
|
||||
final totalSpacing = (column.children.length - 1) * column.spacing;
|
||||
final remainingHeight = size.height - totalFixedHeight - totalSpacing;
|
||||
|
||||
final totalFlex =
|
||||
flexValues.isNotEmpty ? flexValues.reduce((a, b) => a + b) : 0;
|
||||
final flexibleHeights = <double>[];
|
||||
if (totalFlex > 0 && remainingHeight > 0) {
|
||||
for (final flex in flexValues) {
|
||||
flexibleHeights.add((flex / totalFlex) * remainingHeight);
|
||||
}
|
||||
} else {
|
||||
flexibleHeights.addAll(List.filled(flexibleChildren.length, 0.0));
|
||||
}
|
||||
|
||||
final actualHeights = List<double>.filled(column.children.length, 0);
|
||||
for (int i = 0; i < fixedIndices.length; i++) {
|
||||
actualHeights[fixedIndices[i]] = fixedHeights[i];
|
||||
}
|
||||
for (int i = 0; i < flexibleIndices.length; i++) {
|
||||
actualHeights[flexibleIndices[i]] = flexibleHeights[i];
|
||||
}
|
||||
|
||||
double currentY = yOffset;
|
||||
final isRtl = textDirection == TextDirection.rtl;
|
||||
|
||||
for (int i = 0; i < column.children.length; i++) {
|
||||
final child = column.children[i];
|
||||
final childHeight = actualHeights[i];
|
||||
final childWidth = _calculateChildWidth(child, size.width);
|
||||
|
||||
double childX = 0;
|
||||
double effectiveChildWidth = childWidth;
|
||||
|
||||
if (column.crossAxisAlignment == ThermerCrossAxisAlignment.center) {
|
||||
childX = (size.width - effectiveChildWidth) / 2;
|
||||
} else if (column.crossAxisAlignment == ThermerCrossAxisAlignment.end) {
|
||||
childX = isRtl ? 0 : size.width - effectiveChildWidth;
|
||||
} else if (column.crossAxisAlignment == ThermerCrossAxisAlignment.start) {
|
||||
childX = isRtl ? size.width - effectiveChildWidth : 0;
|
||||
} else if (column.crossAxisAlignment ==
|
||||
ThermerCrossAxisAlignment.stretch) {
|
||||
effectiveChildWidth = size.width;
|
||||
childX = 0;
|
||||
} else {
|
||||
childX = (size.width - effectiveChildWidth) / 2;
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(childX, 0);
|
||||
_paintWidget(canvas, Size(effectiveChildWidth, childHeight), child,
|
||||
currentY, linePaint);
|
||||
canvas.restore();
|
||||
|
||||
currentY += childHeight + column.spacing;
|
||||
}
|
||||
}
|
||||
|
||||
double _calculateChildWidth(ThermerWidget child, double availableWidth) {
|
||||
return LayoutUtils.calculateWidgetWidth(child, availableWidth,
|
||||
defaultTextDirection: textDirection);
|
||||
}
|
||||
|
||||
double _calculateChildHeight(ThermerWidget child, double maxWidth) {
|
||||
return LayoutUtils.calculateWidgetHeight(child, maxWidth,
|
||||
defaultTextDirection: textDirection);
|
||||
}
|
||||
|
||||
void _paintDivider(Canvas canvas, Size size, ThermerDivider divider,
|
||||
double yOffset, Paint linePaint) {
|
||||
const dividerStyle = TextStyle(
|
||||
fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500);
|
||||
|
||||
if (divider.isHorizontal) {
|
||||
final length = divider.length ??
|
||||
_calculateDividerLength(divider.character, size.width, dividerStyle);
|
||||
final textPainter = TextMeasurementCache.getPainter(
|
||||
divider.character * length,
|
||||
dividerStyle,
|
||||
TextDirection.ltr,
|
||||
TextAlign.left,
|
||||
null,
|
||||
size.width,
|
||||
);
|
||||
textPainter.paint(canvas, Offset(0, yOffset));
|
||||
} else {
|
||||
final length = divider.length ?? 1;
|
||||
final textPainter = TextMeasurementCache.getPainter(
|
||||
divider.character * length,
|
||||
dividerStyle,
|
||||
TextDirection.ltr,
|
||||
TextAlign.left,
|
||||
null,
|
||||
size.width,
|
||||
);
|
||||
for (int i = 0; i < length; i++) {
|
||||
textPainter.paint(canvas, Offset(0, yOffset + i * textPainter.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _calculateDividerLength(
|
||||
String character, double maxWidth, TextStyle style) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: character,
|
||||
style: style,
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
if (textPainter.width == 0) return 0;
|
||||
return (maxWidth / textPainter.width).floor();
|
||||
}
|
||||
|
||||
void _paintImage(
|
||||
Canvas canvas, Size size, ThermerImage imageWidget, double yOffset) {
|
||||
final srcRect = Rect.fromLTWH(0, 0, imageWidget.image.width.toDouble(),
|
||||
imageWidget.image.height.toDouble());
|
||||
final dstRect = Rect.fromLTWH(0, yOffset, size.width, size.height);
|
||||
canvas.drawImageRect(imageWidget.image, srcRect, dstRect, Paint());
|
||||
}
|
||||
|
||||
void _paintQRCode(
|
||||
Canvas canvas, Size size, ThermerQRCode qrWidget, double yOffset) {
|
||||
canvas.save();
|
||||
canvas.translate(0, yOffset);
|
||||
final qrPainter = QrPainter(
|
||||
data: qrWidget.data,
|
||||
version: QrVersions.auto,
|
||||
errorCorrectionLevel: qrWidget.errorCorrectionLevel,
|
||||
dataModuleStyle: const QrDataModuleStyle(
|
||||
color: Color(0xFF000000),
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
),
|
||||
eyeStyle: const QrEyeStyle(
|
||||
color: Color(0xFF000000),
|
||||
eyeShape: QrEyeShape.square,
|
||||
),
|
||||
);
|
||||
qrPainter.paint(canvas, Size(qrWidget.size, qrWidget.size));
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
void _paintAlign(Canvas canvas, Size size, ThermerAlign alignWidget,
|
||||
double yOffset, Paint linePaint) {
|
||||
final childWidth = LayoutUtils.calculateWidgetWidth(
|
||||
alignWidget.child,
|
||||
size.width,
|
||||
defaultTextDirection: textDirection,
|
||||
);
|
||||
final childHeight = LayoutUtils.calculateWidgetHeight(
|
||||
alignWidget.child,
|
||||
size.width,
|
||||
defaultTextDirection: textDirection,
|
||||
);
|
||||
|
||||
double xOffset = 0;
|
||||
final isRtl = textDirection == TextDirection.rtl;
|
||||
|
||||
switch (alignWidget.alignment) {
|
||||
case ThermerAlignment.left:
|
||||
xOffset = isRtl ? size.width - childWidth : 0;
|
||||
break;
|
||||
case ThermerAlignment.center:
|
||||
xOffset = (size.width - childWidth) / 2;
|
||||
break;
|
||||
case ThermerAlignment.right:
|
||||
xOffset = isRtl ? 0 : size.width - childWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(xOffset, 0);
|
||||
_paintWidget(canvas, Size(childWidth, childHeight), alignWidget.child,
|
||||
yOffset, linePaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export '_layout_utils.dart';
|
||||
export '_thermer_layout.dart';
|
||||
export '_thermer_painter.dart';
|
||||
12
lib/service/thermal_print/thermer/src/widgets/_align.dart
Normal file
12
lib/service/thermal_print/thermer/src/widgets/_align.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import '_base_widget.dart';
|
||||
import '_enums.dart';
|
||||
|
||||
class ThermerAlign extends ThermerWidget {
|
||||
final ThermerWidget child;
|
||||
final ThermerAlignment alignment;
|
||||
|
||||
const ThermerAlign({
|
||||
required this.child,
|
||||
this.alignment = ThermerAlignment.center,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
abstract class ThermerWidget {
|
||||
const ThermerWidget();
|
||||
}
|
||||
19
lib/service/thermal_print/thermer/src/widgets/_column.dart
Normal file
19
lib/service/thermal_print/thermer/src/widgets/_column.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import '_base_widget.dart';
|
||||
import '_enums.dart';
|
||||
|
||||
class ThermerColumn extends ThermerWidget {
|
||||
final List<ThermerWidget> children;
|
||||
|
||||
final ThermerMainAxisAlignment mainAxisAlignment;
|
||||
|
||||
final ThermerCrossAxisAlignment crossAxisAlignment;
|
||||
|
||||
final double spacing;
|
||||
|
||||
const ThermerColumn({
|
||||
required this.children,
|
||||
this.mainAxisAlignment = ThermerMainAxisAlignment.start,
|
||||
this.crossAxisAlignment = ThermerCrossAxisAlignment.start,
|
||||
this.spacing = 3,
|
||||
});
|
||||
}
|
||||
37
lib/service/thermal_print/thermer/src/widgets/_divider.dart
Normal file
37
lib/service/thermal_print/thermer/src/widgets/_divider.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerDivider extends ThermerWidget {
|
||||
final bool isHorizontal;
|
||||
final String character;
|
||||
final int? length;
|
||||
|
||||
ThermerDivider._({
|
||||
required this.isHorizontal,
|
||||
required this.character,
|
||||
required this.length,
|
||||
});
|
||||
|
||||
ThermerDivider copyWith({String? character, int? length}) {
|
||||
return ThermerDivider._(
|
||||
isHorizontal: isHorizontal,
|
||||
character: character ?? this.character,
|
||||
length: length ?? this.length,
|
||||
);
|
||||
}
|
||||
|
||||
factory ThermerDivider.horizontal({String character = '-', int? length}) {
|
||||
return ThermerDivider._(
|
||||
isHorizontal: true,
|
||||
character: character,
|
||||
length: length,
|
||||
);
|
||||
}
|
||||
|
||||
factory ThermerDivider.vertical({String character = '|', int? length}) {
|
||||
return ThermerDivider._(
|
||||
isHorizontal: false,
|
||||
character: character,
|
||||
length: length ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
enum ThermerMainAxisAlignment { start, center, end, spaceBetween, spaceAround, spaceEvenly }
|
||||
|
||||
enum ThermerCrossAxisAlignment { start, center, end, stretch }
|
||||
|
||||
enum ThermerAlignment { left, center, right }
|
||||
11
lib/service/thermal_print/thermer/src/widgets/_expanded.dart
Normal file
11
lib/service/thermal_print/thermer/src/widgets/_expanded.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerExpanded extends ThermerWidget {
|
||||
final ThermerWidget child;
|
||||
final int flex;
|
||||
|
||||
const ThermerExpanded({
|
||||
required this.child,
|
||||
this.flex = 1,
|
||||
}) : assert(flex > 0, 'flex must be greater than 0');
|
||||
}
|
||||
15
lib/service/thermal_print/thermer/src/widgets/_flexible.dart
Normal file
15
lib/service/thermal_print/thermer/src/widgets/_flexible.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import '_base_widget.dart';
|
||||
|
||||
enum ThermerFlexFit { tight, loose }
|
||||
|
||||
class ThermerFlexible extends ThermerWidget {
|
||||
final ThermerWidget child;
|
||||
final int flex;
|
||||
final ThermerFlexFit fit;
|
||||
|
||||
const ThermerFlexible({
|
||||
required this.child,
|
||||
this.flex = 1,
|
||||
this.fit = ThermerFlexFit.loose,
|
||||
}) : assert(flex > 0, 'flex must be greater than 0');
|
||||
}
|
||||
63
lib/service/thermal_print/thermer/src/widgets/_image.dart
Normal file
63
lib/service/thermal_print/thermer/src/widgets/_image.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image/image.dart' as img;
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerImage extends ThermerWidget {
|
||||
final ui.Image image;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const ThermerImage({
|
||||
required this.image,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
|
||||
static Future<ui.Image> _convertImageToUiImage(img.Image image) async {
|
||||
final pngBytes = img.encodePng(image);
|
||||
final codec = await ui.instantiateImageCodec(pngBytes);
|
||||
final frame = await codec.getNextFrame();
|
||||
return frame.image;
|
||||
}
|
||||
|
||||
static Future<ThermerImage> network(
|
||||
String url, {
|
||||
double? width,
|
||||
double? height,
|
||||
}) async {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to load image from $url');
|
||||
}
|
||||
final image = img.decodeImage(response.bodyBytes);
|
||||
if (image == null) {
|
||||
throw Exception('Failed to decode image from $url');
|
||||
}
|
||||
final uiImage = await _convertImageToUiImage(image);
|
||||
return ThermerImage(
|
||||
image: uiImage,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<ThermerImage> memory(
|
||||
Uint8List bytes, {
|
||||
double? width,
|
||||
double? height,
|
||||
}) async {
|
||||
final image = img.decodeImage(bytes);
|
||||
if (image == null) {
|
||||
throw Exception('Failed to decode image from bytes');
|
||||
}
|
||||
final uiImage = await _convertImageToUiImage(image);
|
||||
return ThermerImage(
|
||||
image: uiImage,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
15
lib/service/thermal_print/thermer/src/widgets/_qr_code.dart
Normal file
15
lib/service/thermal_print/thermer/src/widgets/_qr_code.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerQRCode extends ThermerWidget {
|
||||
final String data;
|
||||
final double size;
|
||||
final int errorCorrectionLevel;
|
||||
|
||||
const ThermerQRCode({
|
||||
required this.data,
|
||||
this.size = 100.0,
|
||||
this.errorCorrectionLevel = QrErrorCorrectLevel.L,
|
||||
});
|
||||
}
|
||||
19
lib/service/thermal_print/thermer/src/widgets/_row.dart
Normal file
19
lib/service/thermal_print/thermer/src/widgets/_row.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import '_base_widget.dart';
|
||||
import '_enums.dart';
|
||||
|
||||
class ThermerRow extends ThermerWidget {
|
||||
final List<ThermerWidget> children;
|
||||
|
||||
final ThermerMainAxisAlignment mainAxisAlignment;
|
||||
|
||||
final ThermerCrossAxisAlignment crossAxisAlignment;
|
||||
|
||||
final double spacing;
|
||||
|
||||
const ThermerRow({
|
||||
required this.children,
|
||||
this.mainAxisAlignment = ThermerMainAxisAlignment.start,
|
||||
this.crossAxisAlignment = ThermerCrossAxisAlignment.center,
|
||||
this.spacing = 0,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerSizedBox extends ThermerWidget {
|
||||
final double? width;
|
||||
|
||||
final double? height;
|
||||
|
||||
final ThermerWidget? child;
|
||||
|
||||
const ThermerSizedBox({this.width, this.height, this.child});
|
||||
|
||||
const ThermerSizedBox.square({
|
||||
double dimension = 0,
|
||||
this.child,
|
||||
}) : width = dimension,
|
||||
height = dimension;
|
||||
}
|
||||
46
lib/service/thermal_print/thermer/src/widgets/_table.dart
Normal file
46
lib/service/thermal_print/thermer/src/widgets/_table.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerTableRow {
|
||||
final List<ThermerWidget> cells;
|
||||
|
||||
const ThermerTableRow(this.cells);
|
||||
}
|
||||
|
||||
class ThermerTable extends ThermerWidget {
|
||||
final List<ThermerTableRow> data;
|
||||
|
||||
final ThermerTableRow? header;
|
||||
|
||||
final Map<int, double?>? cellWidths;
|
||||
|
||||
final double columnSpacing;
|
||||
|
||||
final double rowSpacing;
|
||||
|
||||
final TextStyle? style;
|
||||
|
||||
final TextStyle? headerStyle;
|
||||
|
||||
final String horizontalBorderChar;
|
||||
|
||||
final String verticalBorderChar;
|
||||
|
||||
final bool enableHeaderBorders;
|
||||
|
||||
final bool enableTableBorders;
|
||||
|
||||
const ThermerTable({
|
||||
required this.data,
|
||||
this.header,
|
||||
this.cellWidths,
|
||||
this.columnSpacing = 10.0,
|
||||
this.rowSpacing = 3.0,
|
||||
this.style,
|
||||
this.headerStyle,
|
||||
this.horizontalBorderChar = '-',
|
||||
this.verticalBorderChar = '|',
|
||||
this.enableHeaderBorders = true,
|
||||
this.enableTableBorders = false,
|
||||
});
|
||||
}
|
||||
26
lib/service/thermal_print/thermer/src/widgets/_text.dart
Normal file
26
lib/service/thermal_print/thermer/src/widgets/_text.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerText extends ThermerWidget {
|
||||
ThermerText(
|
||||
this.data, {
|
||||
this.direction,
|
||||
this.style = const TextStyle(color: Color(0xFF000000), fontWeight: FontWeight.w500),
|
||||
this.textAlign = TextAlign.left,
|
||||
this.maxLines,
|
||||
this.fallbackFonts,
|
||||
});
|
||||
|
||||
final String data;
|
||||
|
||||
final TextDirection? direction;
|
||||
|
||||
final TextStyle? style;
|
||||
|
||||
final TextAlign textAlign;
|
||||
|
||||
final int? maxLines;
|
||||
|
||||
final List<String>? fallbackFonts;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export '_align.dart';
|
||||
export '_base_widget.dart';
|
||||
export '_column.dart';
|
||||
export '_divider.dart';
|
||||
export '_enums.dart';
|
||||
export '_expanded.dart';
|
||||
export '_flexible.dart';
|
||||
export '_image.dart';
|
||||
export '_qr_code.dart';
|
||||
export '_row.dart';
|
||||
export '_sized_box.dart';
|
||||
export '_table.dart';
|
||||
export '_text.dart';
|
||||
6
lib/service/thermal_print/thermer/thermer.dart
Normal file
6
lib/service/thermal_print/thermer/thermer.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
library;
|
||||
|
||||
export 'src/layouts/layouts.export.dart';
|
||||
export 'src/widgets/widgets.export.dart';
|
||||
export 'package:flutter/material.dart'
|
||||
show TextStyle, Color, Colors, TextAlign, FontWeight, TextDecoration, TextDirection;
|
||||
Reference in New Issue
Block a user