first commit

This commit is contained in:
2026-02-07 15:57:09 +07:00
commit 157096f164
1153 changed files with 415766 additions and 0 deletions

View File

@@ -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._();
// }

View 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;
}
}

View File

@@ -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,
});
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
export '_layout_utils.dart';
export '_thermer_layout.dart';
export '_thermer_painter.dart';

View 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,
});
}

View File

@@ -0,0 +1,3 @@
abstract class ThermerWidget {
const ThermerWidget();
}

View 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,
});
}

View 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,
);
}
}

View File

@@ -0,0 +1,5 @@
enum ThermerMainAxisAlignment { start, center, end, spaceBetween, spaceAround, spaceEvenly }
enum ThermerCrossAxisAlignment { start, center, end, stretch }
enum ThermerAlignment { left, center, right }

View 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');
}

View 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');
}

View 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,
);
}
}

View 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,
});
}

View 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,
});
}

View File

@@ -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;
}

View 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,
});
}

View 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;
}

View File

@@ -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';

View 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;