blue_thermal_plus 0.0.4 copy "blue_thermal_plus: ^0.0.4" to clipboard
blue_thermal_plus: ^0.0.4 copied to clipboard

Plugin para da suporte a impressão android e IOS em impressoras termicas

example/lib/main.dart

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:blue_thermal_plus/src/blue_thermal_plus.dart';
import 'package:blue_thermal_plus/api/models.dart';
import 'package:blue_thermal_plus/api/printer_config.dart';
import 'package:permission_handler/permission_handler.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BlueThermalPlus Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true),
      home: const PluginTestPage(),
    );
  }
}

class PluginTestPage extends StatefulWidget {
  const PluginTestPage({super.key});

  @override
  State<PluginTestPage> createState() => _PluginTestPageState();
}

class _PluginTestPageState extends State<PluginTestPage> {
  final bt = BlueThermalPlus();

  PrinterTransport transport = PrinterTransport.ble;

  final Map<String, PrinterDevice> devices = {};
  PrinterDevice? selected;

  bool scanning = false;
  bool connected = false;
  bool ready = false;

  String status = "Pronto";
  final List<String> logs = [];

  StreamSubscription<PrinterEvent>? sub;

  // ---------------- CONFIG STATE ----------------
  PrinterConfig config = const PrinterConfig();

  bool autoApplyOnScan = true;
  bool autoApplyOnConnect = true;

  // ---------------- STRATEGY ----------------
  final IPrinterStrategy bigStrategy = AutoCtbTestPrinterStrategy();

  @override
  void initState() {
    super.initState();
    sub = bt.events.listen(_handleEvent);
  }

  @override
  void dispose() {
    sub?.cancel();
    super.dispose();
  }

  // ---------------- HELPER: PERMISSIONS ----------------
  Future<bool> _checkPermissions() async {
    if (Platform.isAndroid) {
      if (await Permission.bluetoothScan.request().isGranted &&
          await Permission.bluetoothConnect.request().isGranted) {
        return true;
      }

      if (await Permission.location.request().isGranted) {
        return true;
      }

      _log("❌ Permissões negadas. Verifique as configurações.");
      return false;
    }
    return true;
  }

  // ---------------- LOG + EVENTS ----------------
  void _log(String msg) {
    final line = "${DateTime.now().toIso8601String().substring(11, 19)}  $msg";
    setState(() {
      logs.insert(0, line);
      if (logs.length > 200) logs.removeRange(200, logs.length);
    });
  }

  void _handleEvent(PrinterEvent e) {
    final type = e.type.toString();

    if (e.message != null && e.message!.isNotEmpty) {
      _log("[$type] ${e.message}");
      setState(() => status = e.message!);
    } else {
      _log("[$type]");
    }

    if (e.device != null) {
      final d = e.device!;
      setState(() => devices[d.id] = d);
      _log("[deviceFound] ${d.name} (${d.id})");
    }

    if (type.contains("scanStarted")) {
      setState(() => scanning = true);
    } else if (type.contains("scanStopped")) {
      setState(() => scanning = false);
    } else if (type.contains("connected")) {
      setState(() => connected = true);
    } else if (type.contains("disconnected")) {
      setState(() {
        connected = false;
        ready = false;
      });
    } else if (type.contains("ready")) {
      setState(() => ready = true);
    }
  }

  // ---------------- ACTIONS ----------------
  Future<void> _startScan() async {
    final hasPermission = await _checkPermissions();
    if (!hasPermission) return;

    setState(() {
      devices.clear();
      selected = null;
      scanning = true;
      status = "Iniciando scan...";
    });

    _log(">>> startScan($transport)");
    await bt.startScan(transport: transport);
  }

  Future<void> _stopScan() async {
    _log(">>> stopScan($transport)");
    await bt.stopScan(transport: transport);
    setState(() => scanning = false);
  }

  Future<void> _snapshot() async {
    final hasPermission = await _checkPermissions();
    if (!hasPermission) return;

    _log(">>> getDiscoveredDevices($transport)");
    final list = await bt.getDiscoveredDevices(transport: transport);

    setState(() {
      for (final d in list) devices[d.id] = d;
    });

    _log("Snapshot: ${list.length} devices");
  }

  Future<void> _connectSelected() async {
    final d = selected;
    if (d == null) {
      _log("⚠️ Selecione um device primeiro");
      return;
    }

    if (Platform.isAndroid) {
      if (!await Permission.bluetoothConnect.request().isGranted) {
        _log("❌ Permissão de conexão Bluetooth negada");
        return;
      }
    }

    final profile = PrinterProfiles.zebra;

    _log(">>> configure(profile=zebra)");
    await bt.configure(profile);

    _log(">>> connect(${d.id}, $transport)");
    await bt.connect(deviceId: d.id, transport: transport);
  }

  Future<void> _disconnect() async {
    _log(">>> disconnect($transport)");
    await bt.disconnect(transport: transport);
  }

  Future<void> _printBigAutoCtb() async {
    if (!ready) {
      _log("⚠️ Ainda não está READY (aguarde 'ready')");
      return;
    }

    // Aqui você pode passar um Map fake, se quiser parametrizar no futuro:
    final fakeData = {
      "ait": "CTB-2026-000123",
      "placa": "ABC1D23",
      "dataHora": "05/02/2026 10:32",
    };

    final bytesList = await bigStrategy.generateBytes(fakeData);
    final data = Uint8List.fromList(bytesList);

    _log(">>> printRawBytes(BIG ${data.length} bytes, $transport)");
    await bt.printRawBytes(data, transport: transport);
  }

  // ---------------- UI ----------------
  @override
  Widget build(BuildContext context) {
    final list = devices.values.toList()
      ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));

    return Scaffold(
      appBar: AppBar(
        title: const Text("BlueThermalPlus - Test App"),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 12),
            child: Center(
              child: Row(
                children: [
                  const Text("Transport: "),
                  const SizedBox(width: 6),
                  DropdownButton<PrinterTransport>(
                    value: transport,
                    items: const [
                      DropdownMenuItem(
                        value: PrinterTransport.ble,
                        child: Text("BLE"),
                      ),
                      DropdownMenuItem(
                        value: PrinterTransport.classic,
                        child: Text("Classic"),
                      ),
                    ],
                    onChanged: (v) {
                      if (v == null) return;
                      setState(() {
                        transport = v;
                        devices.clear();
                        selected = null;
                        scanning = false;
                        connected = false;
                        ready = false;
                        status = "Transport alterado para ${v.name}";
                      });
                      _log("=== Transport => ${v.name} ===");
                    },
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: _StatusCard(
              status: status,
              scanning: scanning,
              connected: connected,
              ready: ready,
              transport: transport,
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12),
            child: Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                FilledButton.icon(
                  onPressed: scanning ? null : _startScan,
                  icon: const Icon(Icons.radar),
                  label: const Text("Start scan"),
                ),
                OutlinedButton.icon(
                  onPressed: scanning ? _stopScan : null,
                  icon: const Icon(Icons.stop),
                  label: const Text("Stop scan"),
                ),
                OutlinedButton.icon(
                  onPressed: _snapshot,
                  icon: const Icon(Icons.refresh),
                  label: const Text("Snapshot"),
                ),
                FilledButton.icon(
                  onPressed: selected == null ? null : _connectSelected,
                  icon: const Icon(Icons.link),
                  label: const Text("Connect"),
                ),
                OutlinedButton.icon(
                  onPressed: connected ? _disconnect : null,
                  icon: const Icon(Icons.link_off),
                  label: const Text("Disconnect"),
                ),

                // ✅ NOVO BOTÃO: impressão grande
                FilledButton.icon(
                  onPressed: ready ? _printBigAutoCtb : null,
                  icon: const Icon(Icons.receipt_long),
                  label: const Text("Print BIG (Auto CTB)"),
                ),
              ],
            ),
          ),
          const SizedBox(height: 8),
          Expanded(
            child: Row(
              children: [
                Expanded(
                  flex: 2,
                  child: _DeviceList(
                    devices: list,
                    selectedId: selected?.id,
                    onSelect: (d) {
                      setState(() => selected = d);
                      _log("Selecionado: ${d.name} (${d.id})");
                    },
                  ),
                ),
                Expanded(flex: 2, child: _LogPanel(logs: logs)),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ==================== STRATEGY (SEU CÓDIGO) ====================

abstract class IPrinterStrategy {
  Future<List<int>> generateBytes(dynamic data);
}

class AutoCtbTestPrinterStrategy implements IPrinterStrategy {
  @override
  Future<List<int>> generateBytes(dynamic data) async {
    final cpcl = <int>[];
    void add(String s) => cpcl.addAll(s.codeUnits);

    // Começo "limpo"
    add("\r\n");

    add("\r\n");
    add("! 0 200 200 4000 1\r\n");
    add("PAGE-WIDTH 800\r\n");
    add("JOURNAL\r\n");
    add("T 5 0 30 30 TESTE DE IMPRESSAO 2\r\n");
    // add("T 7 0 30 80 Modulo Clean Arch\r\n");
    // add("T 7 0 30 120 Modulo Clean Arch\r\n");
    int y = 80;

    void row(String produto, String valor) {
      add("T 7 0 30 $y $produto: $valor\r\n");
      y += 30;
    }

    row("Arroz 5kg", "R\$ 28,90");
    row("Feijão carioca 1kg", "R\$ 8,50");
    row("Macarrão espaguete", "R\$ 4,20");
    row("Óleo de soja 900ml", "R\$ 7,80");
    row("Açúcar refinado 1kg", "R\$ 5,10");
    row("Café torrado 500g", "R\$ 14,90");
    row("Leite integral 1L", "R\$ 4,80");
    row("Margarina 500g", "R\$ 6,40");
    row("Farinha de trigo 1kg", "R\$ 4,60");
    row("Biscoito recheado", "R\$ 3,90");
    row("Molho de tomate", "R\$ 2,70");
    row("Refrigerante 2L", "R\$ 8,99");
    row("Suco de laranja 1L", "R\$ 6,50");
    row("Queijo muçarela 300g", "R\$ 12,80");
    row("Presunto fatiado", "R\$ 9,40");
    row("Frango congelado kg", "R\$ 11,90");
    row("Carne moída kg", "R\$ 24,90");
    row("Ovos dúzia", "R\$ 9,20");
    row("Pão de forma", "R\$ 7,30");
    row("Manteiga 200g", "R\$ 8,60");
    row("Iogurte natural", "R\$ 3,50");
    row("Cereal matinal", "R\$ 13,40");
    row("Achocolatado", "R\$ 6,90");
    row("Sabão em pó", "R\$ 15,80");
    row("Amaciante roupas", "R\$ 12,70");
    row("Detergente líquido", "R\$ 2,30");
    row("Papel higiênico 12un", "R\$ 18,90");
    row("Shampoo", "R\$ 14,50");
    row("Condicionador", "R\$ 15,20");
    row("Creme dental", "R\$ 6,10");
    row("Escova de dentes", "R\$ 5,90");
    row("Sabonete", "R\$ 2,20");
    row("Água mineral 1,5L", "R\$ 2,80");
    row("Chocolate barra", "R\$ 7,50");
    row("Sorvete 2L", "R\$ 19,90");
    row("Pizza congelada", "R\$ 17,80");
    row("Hambúrguer pacote", "R\$ 13,60");
    row("Batata frita", "R\$ 9,90");
    row("Milho verde lata", "R\$ 4,10");
    row("Ervilha lata", "R\$ 4,00");
    row("Atum lata", "R\$ 8,30");
    row("Sardinha lata", "R\$ 6,70");
    row("Maionese", "R\$ 7,20");
    row("Ketchup", "R\$ 6,40");
    row("Mostarda", "R\$ 5,80");
    row("Tempero completo", "R\$ 4,90");
    row("Sal refinado", "R\$ 2,10");
    row("Pimenta molho", "R\$ 6,00");
    row("Bala sortida", "R\$ 3,20");
    row("Chiclete pacote", "R\$ 2,90");
    row("Produto ULTIMO", "R\$ 20,00");
    row("Arroz 5kg", "R\$ 28,90");
    row("Feijão carioca 1kg", "R\$ 8,50");
    row("Macarrão espaguete", "R\$ 4,20");
    row("Óleo de soja 900ml", "R\$ 7,80");
    row("Açúcar refinado 1kg", "R\$ 5,10");
    row("Café torrado 500g", "R\$ 14,90");
    row("Leite integral 1L", "R\$ 4,80");
    row("Margarina 500g", "R\$ 6,40");
    row("Farinha de trigo 1kg", "R\$ 4,60");
    row("Biscoito recheado", "R\$ 3,90");
    row("Molho de tomate", "R\$ 2,70");
    row("Refrigerante 2L", "R\$ 8,99");
    row("Suco de laranja 1L", "R\$ 6,50");
    row("Queijo muçarela 300g", "R\$ 12,80");
    row("Presunto fatiado", "R\$ 9,40");
    row("Frango congelado kg", "R\$ 11,90");
    row("Carne moída kg", "R\$ 24,90");
    row("Ovos dúzia", "R\$ 9,20");
    row("Pão de forma", "R\$ 7,30");
    row("Manteiga 200g", "R\$ 8,60");
    row("Iogurte natural", "R\$ 3,50");
    row("Cereal matinal", "R\$ 13,40");
    row("Achocolatado", "R\$ 6,90");
    row("Sabão em pó", "R\$ 15,80");
    row("Amaciante roupas", "R\$ 12,70");
    row("Detergente líquido", "R\$ 2,30");
    row("Papel higiênico 12un", "R\$ 18,90");
    row("Shampoo", "R\$ 14,50");
    row("Condicionador", "R\$ 15,20");
    row("Creme dental", "R\$ 6,10");
    row("Escova de dentes", "R\$ 5,90");
    row("Sabonete", "R\$ 2,20");
    row("Água mineral 1,5L", "R\$ 2,80");
    row("Chocolate barra", "R\$ 7,50");
    row("Sorvete 2L", "R\$ 19,90");
    row("Pizza congelada", "R\$ 17,80");
    row("Hambúrguer pacote", "R\$ 13,60");
    row("Batata frita", "R\$ 9,90");
    row("Milho verde lata", "R\$ 4,10");
    row("Ervilha lata", "R\$ 4,00");
    row("Atum lata", "R\$ 8,30");
    row("Sardinha lata", "R\$ 6,70");
    row("Maionese", "R\$ 7,20");
    row("Ketchup", "R\$ 6,40");
    row("Mostarda", "R\$ 5,80");
    row("Tempero completo", "R\$ 4,90");
    row("Sal refinado", "R\$ 2,10");
    row("Pimenta molho", "R\$ 6,00");
    row("Bala sortida", "R\$ 3,20");
    row("Chiclete pacote", "R\$ 2,90");
    row("Produto ULTIMO", "R\$ 20,00");

    add("LINE 30 160 750 160 3\r\n");
    add("BARCODE 128 1 1 50 30 400 123456789\r\n");
    add("PRINT\r\n");

    return cpcl;
  }
}

// class AutoCtbTestPrinterStrategy implements IPrinterStrategy {
//   @override
//   Future<List<int>> generateBytes(dynamic data) async {
//     final cpcl = <int>[];
//     // Helper para converter string em bytes CPCL
//     void add(String s) => cpcl.addAll(s.codeUnits);
//
//     // --- COMANDOS INICIAIS ---
//     add("\r\n");
//     add("! 0 200 200 4000 1\r\n");
//     add("PAGE-WIDTH 800\r\n");
//     add("JOURNAL\r\n"); // Modo Journal para impressão contínua
//
//     // --- FUNÇÃO AJUDANTE PARA TEXTO MULTILINHA (NO DART) ---
//     // Isso substitui o comando MULTILINE que trava a impressora.
//     // Retorna o novo Y após escrever as linhas.
//     int printMultiline(String text, int x, int startY, int maxCharsPerLine) {
//       int currentY = startY;
//       List<String> words = text.split(' ');
//       String currentLine = "";
//
//       for (var word in words) {
//         if ((currentLine + word).length > maxCharsPerLine) {
//           // Imprime a linha acumulada
//           add("T 7 0 $x $currentY $currentLine\r\n");
//           currentY += 30; // Avança 30px para baixo
//           currentLine = "";
//         }
//         currentLine += "$word ";
//       }
//       // Imprime o que sobrou
//       if (currentLine.isNotEmpty) {
//         add("T 7 0 $x $currentY $currentLine\r\n");
//         currentY += 30;
//       }
//       return currentY;
//     }
//
//     // =================================================
//     // HEADER
//     // =================================================
//     add("T 5 1 0 20 NOTIFICACAO DE AUTUACAO\r\n");
//     add("T 7 0 0 60 AUTO CTB - TESTE\r\n");
//     add("T 7 0 30 110 AIT: CTB-2026-000123\r\n");
//     add("T 7 0 30 140 DATA/HORA: 05/02/2026 10:32\r\n");
//     add("LINE 30 175 750 175 3\r\n");
//
//     // =================================================
//     // VEICULO
//     // =================================================
//     add("T 5 0 30 195 VEICULO\r\n");
//     add("T 7 0 30 230 PLACA: ABC1D23\r\n");
//     add("T 7 0 30 260 MARCA/MODELO: FIAT ARGO\r\n");
//     add("T 7 0 30 290 COR: BRANCO\r\n");
//     add("T 7 0 30 320 ANO: 2022\r\n");
//     add("LINE 30 350 750 350 3\r\n");
//
//     // =================================================
//     // CONDUTOR
//     // =================================================
//     add("T 5 0 30 370 CONDUTOR\r\n");
//     add("T 7 0 30 405 NOME: JOAO DA SILVA\r\n");
//     add("T 7 0 30 435 CNH: 12345678900\r\n");
//     add("LINE 30 465 750 465 3\r\n");
//
//     // =================================================
//     // LOCAL
//     // =================================================
//     add("T 5 0 30 485 LOCAL\r\n");
//     add("T 7 0 30 520 AV PAULISTA, 1000 - SP\r\n");
//     add("T 7 0 30 550 SENTIDO: CENTRO -> JARDINS\r\n");
//     add("LINE 30 580 750 580 3\r\n");
//
//     // =================================================
//     // INFRACAO
//     // =================================================
//     add("T 5 0 30 600 INFRACAO\r\n");
//     add("T 7 0 30 635 COD: 74550\r\n");
//     add("T 7 0 30 665 ART: 181 XVIII\r\n");
//     add("T 7 0 30 695 GRAV: MEDIA\r\n");
//
//     // SUBSTITUÍDO: MULTILINE nativo por lógica manual
//     // Texto longo da infração
//     String textoInfracao = "ESTACIONAR EM LOCAL PROIBIDO SINALIZADO";
//     // Imprime e calcula onde a linha termina (baseado em max 40 caracteres por linha)
//     int yAposInfracao = printMultiline(textoInfracao, 30, 725, 40);
//
//     // Ajustamos a linha abaixo baseada em onde o texto acabou
//     // (Ou mantemos fixa se soubermos que não vai estourar)
//     add("LINE 30 820 750 820 3\r\n");
//
//     // =================================================
//     // MEDICOES
//     // =================================================
//     add("T 5 0 30 840 MEDICOES\r\n");
//     add("T 7 0 30 875 VEL PERMITIDA: 60 KM/H\r\n");
//     add("T 7 0 30 905 VEL AFERIDA: ---\r\n");
//     add("LINE 30 935 750 935 3\r\n");
//
//     // =================================================
//     // OBSERVACOES
//     // =================================================
//     add("T 5 0 30 955 OBSERVACOES\r\n");
//
//     // SUBSTITUÍDO: MULTILINE nativo por lógica manual
//     String textoObs = "IMPRESSAO DE TESTE DO MODULO CLEAN ARCH";
//     printMultiline(textoObs, 30, 985, 40);
//
//     add("LINE 30 1080 750 1080 3\r\n");
//
//     // =================================================
//     // AGENTE
//     // =================================================
//     add("T 5 0 30 1100 AGENTE\r\n");
//     add("T 7 0 30 1135 AGENTE MARIA PEREIRA\r\n");
//     add("T 7 0 30 1165 MAT: 009988\r\n");
//
//     // =================================================
//     // 2ª VIA - INFORMACOES AO USUARIO
//     // =================================================
//     add("LINE 30 1200 750 1200 3\r\n");
//
//     // Removi CENTER/LEFT para garantir estabilidade, usando coordenada X centralizada manualmente ou 0
//     // CENTER as vezes conflita com coordenadas explícitas em alguns firmwares
//     add("T 5 0 100 1220 2a VIA - INFORMACOES AO USUARIO\r\n");
//
//     String textoLegal = "DOCUMENTO DE TESTE. CONFIRA OS DADOS IMPRESSOS.";
//     printMultiline(textoLegal, 30, 1260, 40);
//
//     // =================================================
//     // BARCODE DE TESTE
//     // =================================================
//     // Aumentei um pouco o Y para garantir que não sobreponha o texto acima
//     add("BARCODE 128 1 1 60 30 1350 CTB2026000123\r\n");
//
//     // Finaliza a impressão
//     add("PRINT\r\n");
//
//     return cpcl;
//   }
// }

// ==================== UI WIDGETS (IGUAIS) ====================

class _StatusCard extends StatelessWidget {
  final String status;
  final bool scanning;
  final bool connected;
  final bool ready;
  final PrinterTransport transport;

  const _StatusCard({
    required this.status,
    required this.scanning,
    required this.connected,
    required this.ready,
    required this.transport,
  });

  @override
  Widget build(BuildContext context) {
    final icon = connected ? Icons.print : Icons.bluetooth_searching;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Row(
          children: [
            Icon(icon, size: 36),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    "Status",
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 4),
                  Text(status),
                  const SizedBox(height: 8),
                  Wrap(
                    spacing: 8,
                    children: [
                      Chip(label: Text("transport: ${transport.name}")),
                      Chip(label: Text(scanning ? "scanning" : "idle")),
                      Chip(
                        label: Text(connected ? "connected" : "disconnected"),
                      ),
                      Chip(label: Text(ready ? "ready" : "not ready")),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _DeviceList extends StatelessWidget {
  final List<PrinterDevice> devices;
  final String? selectedId;
  final void Function(PrinterDevice d) onSelect;

  const _DeviceList({
    required this.devices,
    required this.selectedId,
    required this.onSelect,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.fromLTRB(12, 0, 6, 12),
      child: Column(
        children: [
          const ListTile(
            title: Text("Dispositivos"),
            subtitle: Text("Clique para selecionar"),
            dense: true,
          ),
          const Divider(height: 1),
          Expanded(
            child: devices.isEmpty
                ? const Center(child: Text("Nenhum device ainda"))
                : ListView.builder(
                    itemCount: devices.length,
                    itemBuilder: (context, i) {
                      final d = devices[i];
                      final selected = d.id == selectedId;

                      return ListTile(
                        selected: selected,
                        leading: const Icon(Icons.bluetooth),
                        title: Text(d.name),
                        subtitle: Text(d.id),
                        trailing: selected
                            ? const Icon(Icons.check_circle)
                            : null,
                        onTap: () => onSelect(d),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

class _LogPanel extends StatelessWidget {
  final List<String> logs;

  const _LogPanel({required this.logs});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.fromLTRB(6, 0, 12, 12),
      child: Column(
        children: [
          const ListTile(
            title: Text("Logs / Events"),
            subtitle: Text("Realtime do plugin"),
            dense: true,
          ),
          const Divider(height: 1),
          Expanded(
            child: logs.isEmpty
                ? const Center(child: Text("Sem logs ainda"))
                : ListView.builder(
                    itemCount: logs.length,
                    itemBuilder: (context, i) {
                      final l = logs[i];
                      return Padding(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 8,
                          vertical: 4,
                        ),
                        child: Text(
                          l,
                          style: const TextStyle(
                            fontFamily: "monospace",
                            fontSize: 12,
                          ),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}
2
likes
160
points
201
downloads

Publisher

unverified uploader

Weekly Downloads

Plugin para da suporte a impressão android e IOS em impressoras termicas

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, permission_handler, plugin_platform_interface

More

Packages that depend on blue_thermal_plus

Packages that implement blue_thermal_plus