nf5503_flutter 0.0.2 copy "nf5503_flutter: ^0.0.2" to clipboard
nf5503_flutter: ^0.0.2 copied to clipboard

PlatformAndroid

M5Stack NF5503 Android barcode scanner and thermal/label printer Flutter plugin.

example/lib/main.dart

import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:nf5503_flutter/nf5503_flutter.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

enum _PrintPaperType { receipt, label }

enum _ActionTone { filled, tinted, plain }

class _ScanRecord {
  const _ScanRecord({
    required this.content,
    required this.receivedAt,
    this.action,
    this.extras = const <String, Object?>{},
  });

  final String content;
  final DateTime receivedAt;
  final String? action;
  final Map<String, Object?> extras;

  String get time {
    return '${receivedAt.hour.toString().padLeft(2, '0')}:'
        '${receivedAt.minute.toString().padLeft(2, '0')}:'
        '${receivedAt.second.toString().padLeft(2, '0')}';
  }
}

class _MyAppState extends State<MyApp> {
  static const _defaultScanAction = 'com.m5stack.nf5503_flutter.SCAN';
  static const _defaultScanKey = 'barcode';
  static const _thermalCalibrationDensities = <int>[
    4,
    8,
    12,
    16,
    20,
    24,
    28,
    32,
    36,
    40,
  ];

  final _plugin = Nf5503Flutter();
  final _scanActionController = TextEditingController(text: _defaultScanAction);
  final _scanKeyController = TextEditingController(text: _defaultScanKey);
  final _blackMarkThresholdController = TextEditingController(text: '120');
  final _logs = <String>[];

  StreamSubscription<Nf5503ScanResult>? _scanSubscription;
  StreamSubscription<Nf5503PrintEvent>? _printSubscription;

  String _platformVersion = 'Unknown';
  String _scannerMode = '-';
  String _lastScan = '等待扫码结果';
  Nf5503ScanResult? _lastScanResult;
  final _scanRecords = <_ScanRecord>[];
  String _printerVersion = 'Unknown';
  bool? _scannerOpen;
  bool? _printerOpen;
  int? _supportPrint;
  int? _lastPrinterState;
  bool _printServiceEnabled = false;
  bool _blackMarkThresholdEnabled = true;
  _PrintPaperType _printPaperType = _PrintPaperType.receipt;
  String _printFontName = 'Roboto-Regular';
  int _thermalCalibrationDensity = 25;
  bool _busy = false;

  @override
  void initState() {
    super.initState();
    unawaited(_loadPlatformVersion());
  }

  @override
  void dispose() {
    _scanSubscription?.cancel();
    _printSubscription?.cancel();
    _scanActionController.dispose();
    _scanKeyController.dispose();
    _blackMarkThresholdController.dispose();
    super.dispose();
  }

  Future<void> _loadPlatformVersion() async {
    try {
      final version = await _plugin.getPlatformVersion();
      if (!mounted) {
        return;
      }
      setState(() => _platformVersion = version ?? 'Unknown');
      _log('平台版本: $_platformVersion');
    } on PlatformException catch (error) {
      _log('读取平台版本失败: ${error.message ?? error.code}');
    } on MissingPluginException catch (error) {
      _log('当前运行环境未注册插件: ${error.message ?? error.toString()}');
    } catch (error) {
      _log('读取平台版本失败: $error');
    }
  }

  Future<void> _run(String label, Future<void> Function() action) async {
    if (_busy) {
      return;
    }
    setState(() => _busy = true);
    _log('开始: $label');
    try {
      await action();
      _log('完成: $label');
    } catch (error, stackTrace) {
      _log('失败: $label -> $error');
      debugPrintStack(stackTrace: stackTrace);
    } finally {
      if (mounted) {
        setState(() => _busy = false);
      }
    }
  }

  void _log(String message) {
    if (!mounted) {
      return;
    }
    final now = DateTime.now();
    final time =
        '${now.hour.toString().padLeft(2, '0')}:'
        '${now.minute.toString().padLeft(2, '0')}:'
        '${now.second.toString().padLeft(2, '0')}';
    setState(() {
      _logs.insert(0, '[$time] $message');
      if (_logs.length > 80) {
        _logs.removeLast();
      }
    });
  }

  Future<void> _prepareScanner() async {
    final action = _scanActionController.text.trim();
    final key = _scanKeyController.text.trim();
    if (action.isEmpty || key.isEmpty) {
      throw StateError('广播 Action 和 Key 不能为空');
    }

    await _scanSubscription?.cancel();
    await _plugin.scanner.setBroadcastAction(action);
    await _plugin.scanner.setBroadcastKey(key);
    await _plugin.scanner.setOutputMode(Nf5503ScanOutputMode.broadcast);
    await _plugin.scanner.setEndMark(Nf5503ScanEndMark.none);
    await _plugin.scanner.setPlaySound(true);
    await _plugin.scanner.setVibrate(true);

    final openResult = await _plugin.scanner.open();
    final isOpen = await _plugin.scanner.isOpen();
    final mode = await _plugin.scanner.getOutputMode();

    _scanSubscription = _plugin.scanner
        .results(action: action, key: key)
        .listen((result) {
          if (!mounted) {
            return;
          }
          final scanText = result.data.isEmpty ? '(空扫码内容)' : result.data;
          setState(() {
            _lastScan = scanText;
            _lastScanResult = result;
            _scanRecords.insert(
              0,
              _ScanRecord(
                content: scanText,
                action: result.action,
                extras: result.extras,
                receivedAt: DateTime.now(),
              ),
            );
            if (_scanRecords.length > 20) {
              _scanRecords.removeLast();
            }
          });
          _log('扫码内容: $scanText');
        }, onError: (Object error) => _log('扫码监听错误: $error'));

    setState(() {
      _scannerOpen = isOpen;
      _scannerMode = mode.name;
      _lastScan = '已监听 $action / $key,按“开始扫码”或实体扫码键测试';
    });
    _log('扫码 SDK 初始化: open=$openResult, isOpen=$isOpen, mode=${mode.name}');
  }

  Future<void> _startDecode() async {
    final success = await _plugin.scanner.startDecode();
    _log('startDecode 返回: $success');
  }

  Future<void> _stopDecode() async {
    final success = await _plugin.scanner.stopDecode();
    _log('stopDecode 返回: $success');
  }

  Future<void> _readScannerConfig() async {
    final isOpen = await _plugin.scanner.isOpen();
    final mode = await _plugin.scanner.getOutputMode();
    final action = await _plugin.scanner.getBroadcastAction();
    final key = await _plugin.scanner.getBroadcastKey();
    final sound = await _plugin.scanner.getPlaySound();
    final vibrate = await _plugin.scanner.getVibrate();
    setState(() {
      _scannerOpen = isOpen;
      _scannerMode = mode.name;
    });
    _log(
      '扫码配置: open=$isOpen, mode=${mode.name}, action=$action, key=$key, sound=$sound, vibrate=$vibrate',
    );
  }

  Future<void> _closeScanner() async {
    await _scanSubscription?.cancel();
    _scanSubscription = null;
    final success = await _plugin.scanner.close();
    setState(() => _scannerOpen = false);
    _log('closeScanner 返回: $success');
  }

  Future<void> _listenPrinterEvents() async {
    await _printSubscription?.cancel();
    _printSubscription = _plugin.printer.events().listen((event) {
      if (event.type == Nf5503PrintEventType.version) {
        setState(() => _printerVersion = event.version ?? 'Unknown');
        _log('打印版本回调: ${event.version ?? 'Unknown'}');
        return;
      }
      setState(() => _lastPrinterState = event.state);
      _log(
        '打印状态回调: state=${event.state}, code=${event.errorCode?.name ?? 'unknown'}',
      );
    }, onError: (Object error) => _log('打印监听错误: $error'));
    _log('已启动打印事件监听');
  }

  Future<void> _openPrinter() async {
    await _listenPrinterEvents();
    final openResult = await _plugin.printer.open();
    final version = await _plugin.printer.getVersion();
    final supportPrint = await _plugin.printer.getSupportPrint();
    final concentration = await _plugin.printer.getConcentration();
    final fontType = await _plugin.printer.getFontType();
    setState(() {
      _printerOpen = openResult;
      _printServiceEnabled = openResult;
      _printerVersion = version;
      _supportPrint = supportPrint;
      _printFontName = fontType.isEmpty ? _printFontName : fontType;
      _thermalCalibrationDensity = concentration.clamp(1, 40).toInt();
    });
    _log(
      '打印 SDK 打开: open=$openResult, version=$version, support=$supportPrint, density=$concentration',
    );
  }

  Future<void> _setPrintServiceEnabled(bool enabled) async {
    if (enabled) {
      await _openPrinter();
      await _applyPrinterCommonSettings();
      return;
    }
    await _closePrinter();
  }

  Future<void> _setPaperType(_PrintPaperType type) async {
    setState(() => _printPaperType = type);
    await _plugin.printer.open();
    await _plugin.printer.setBlackMark(type == _PrintPaperType.label);
    setState(() {
      _printerOpen = true;
      _printServiceEnabled = true;
    });
    _log('打印纸类型: ${type == _PrintPaperType.label ? '热敏标签纸' : '热敏小票纸'}');
  }

  Future<void> _setPrintDensity(int density) async {
    await _plugin.printer.open();
    await _plugin.printer.setConcentration(density);
    setState(() {
      _printerOpen = true;
      _printServiceEnabled = true;
    });
    _log('当前打印浓度已设置: $density');
  }

  Future<void> _selectPrintFont() async {
    final selectedFont = await showCupertinoModalPopup<String>(
      context: context,
      builder: (context) {
        return CupertinoActionSheet(
          title: const Text('选择打印字体'),
          message: const Text('与官方 demo 的字体配置项保持一致'),
          actions: [
            for (final font in const ['Roboto-Regular', 'default'])
              CupertinoActionSheetAction(
                onPressed: () => Navigator.of(context).pop(font),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(font),
                    if (font == _printFontName) ...[
                      const SizedBox(width: 8),
                      const Icon(CupertinoIcons.checkmark_alt, size: 18),
                    ],
                  ],
                ),
              ),
          ],
          cancelButton: CupertinoActionSheetAction(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('取消'),
          ),
        );
      },
    );
    if (selectedFont == null) {
      return;
    }
    await _plugin.printer.open();
    await _plugin.printer.setFontType(selectedFont);
    setState(() {
      _printFontName = selectedFont;
      _printerOpen = true;
      _printServiceEnabled = true;
    });
    _log('当前打印字体: $_printFontName');
  }

  Future<void> _applyPrinterCommonSettings() async {
    await _plugin.printer.open();
    await _plugin.printer.setBlackMark(
      _printPaperType == _PrintPaperType.label,
    );
    await _plugin.printer.setConcentration(_thermalCalibrationDensity);
    await _plugin.printer.setReverse(false);
    await _plugin.printer.setUnderline(false);
    await _plugin.printer.setBold(false);
    await _plugin.printer.setLineSpacing(1.0);
    if (_printPaperType == _PrintPaperType.label &&
        _blackMarkThresholdEnabled) {
      await _setBlackMarkThreshold(logResult: false);
    }
    setState(() {
      _printerOpen = true;
      _printServiceEnabled = true;
    });
  }

  Future<void> _setBlackMarkThresholdEnabled(bool enabled) async {
    final targetPaperType = enabled ? _PrintPaperType.label : _printPaperType;
    setState(() => _blackMarkThresholdEnabled = enabled);
    await _plugin.printer.open();
    await _plugin.printer.setBlackMark(
      targetPaperType == _PrintPaperType.label,
    );
    setState(() {
      _printerOpen = true;
      _printServiceEnabled = true;
      _printPaperType = targetPaperType;
    });
    _log('黑标阈值启用开关: $enabled');
  }

  Future<void> _getBlackMarkThreshold() async {
    final blackMark = await _plugin.printer.isBlackMark();
    final state = await _plugin.printer.getState(
      Nf5503PrinterStateType.checkBlackMark,
    );
    setState(() => _lastPrinterState = state);
    _log(
      '黑标状态: enabled=$blackMark, state=$state, 当前输入阈值=${_blackMarkThresholdController.text}',
    );
  }

  Future<void> _setBlackMarkThreshold({bool logResult = true}) async {
    final threshold = _blackMarkThreshold();
    await _plugin.printer.open();
    await _plugin.printer.setBlackMark(true);
    final state = await _plugin.printer.setThreshold(threshold);
    setState(() {
      _printerOpen = true;
      _printServiceEnabled = true;
      _printPaperType = _PrintPaperType.label;
      _lastPrinterState = state;
    });
    if (logResult) {
      _log('黑标阈值已设置: threshold=$threshold, state=$state');
    }
  }

  Future<void> _printBlackMarkTest() async {
    await _applyPrinterCommonSettings();
    await _plugin.printer.addText(
      'Black Mark Threshold Test',
      align: Nf5503PrintAlign.center,
      fontSize: Nf5503PrintFontSize.middle,
      isBold: true,
    );
    await _plugin.printer.addText(
      'Threshold: ${_blackMarkThreshold()}',
      align: Nf5503PrintAlign.center,
    );
    await _plugin.printer.addText(
      DateTime.now().toIso8601String(),
      align: Nf5503PrintAlign.center,
    );
    await _plugin.printer.goToNextMark();
    _log('已提交黑标阈值打印测试');
  }

  Future<void> _autoCalibrateBlackMark() async {
    await _setBlackMarkThreshold(logResult: false);
    await _plugin.printer.goToNextMark();
    final state = await _plugin.printer.getState(
      Nf5503PrinterStateType.checkBlackMark,
    );
    setState(() => _lastPrinterState = state);
    _log('已执行自动校准: threshold=${_blackMarkThreshold()}, state=$state');
  }

  Future<void> _printTextTest() async {
    await _applyPrinterCommonSettings();
    await _plugin.printer.addText(
      'NF5503 Text Print',
      align: Nf5503PrintAlign.center,
      fontSize: Nf5503PrintFontSize.large,
      isBold: true,
    );
    await _plugin.printer.addText('Paper: ${_paperTypeLabel(_printPaperType)}');
    await _plugin.printer.addText('Density: $_thermalCalibrationDensity/40');
    await _plugin.printer.addText('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
    await _plugin.printer.addText('abcdefghijklmnopqrstuvwxyz');
    await _plugin.printer.addText('0123456789');
    await _plugin.printer.addBlankLines(3);
    await _plugin.printer.start();
    _log('已提交文本打印测试');
  }

  Future<void> _printBarcodeTest() async {
    await _applyPrinterCommonSettings();
    await _plugin.printer.addText(
      'Barcode Print',
      align: Nf5503PrintAlign.center,
      fontSize: Nf5503PrintFontSize.middle,
      isBold: true,
    );
    await _plugin.printer.addBarcode(
      content: 'NF5503-123456',
      height: 80,
      type: Nf5503BarcodeType.code128,
      align: Nf5503PrintAlign.center,
      hriPosition: Nf5503HriPosition.below,
    );
    await _plugin.printer.addQrCode(
      'NF5503 Flutter SDK test ${DateTime.now().millisecondsSinceEpoch}',
      align: Nf5503PrintAlign.center,
      size: 240,
    );
    await _plugin.printer.addBlankLines(3);
    await _plugin.printer.start();
    _log('已提交条码打印测试');
  }

  Future<void> _printImageTest() async {
    await _applyPrinterCommonSettings();
    final imageBytes = await _buildPrintImageBytes();
    await _plugin.printer.addImageBytes(
      imageBytes,
      align: Nf5503PrintAlign.center,
    );
    await _plugin.printer.addBlankLines(3);
    await _plugin.printer.start();
    _log('已提交图片打印测试');
  }

  Future<void> _printThermalCalibrationScale() async {
    await _listenPrinterEvents();
    await _plugin.printer.open();
    await _plugin.printer.setBlackMark(false);
    await _plugin.printer.setReverse(false);
    await _plugin.printer.setUnderline(false);
    await _plugin.printer.setBold(false);
    await _plugin.printer.setLineSpacing(1.0);

    for (final density in _thermalCalibrationDensities) {
      await _waitForPrinterIdle();
      await _plugin.printer.setConcentration(density);
      await _plugin.printer.addText(
        'Density $density/40  gray ${_nativeDensityStep(density)}/10',
        align: Nf5503PrintAlign.center,
        fontSize: Nf5503PrintFontSize.small,
        isBold: true,
      );
      await _plugin.printer.addText('The quick brown fox 1234567890');
      await _plugin.printer.addText('############################');
      await _plugin.printer.addText('||||||||||||||||||||||||||||');
      await _plugin.printer.addBlankLines(1);
      await _plugin.printer.start();
      await Future<void>.delayed(const Duration(milliseconds: 600));
      await _waitForPrinterIdle(timeout: const Duration(seconds: 5));
    }

    setState(() => _printerOpen = true);
    _log('已提交热敏浓度校准尺: 4/8/.../40');
  }

  Future<void> _printLabelSample() async {
    setState(() => _printPaperType = _PrintPaperType.label);
    await _applyPrinterCommonSettings();
    await _plugin.printer.setUnwindPaperLength(120);
    await _plugin.printer.addText(
      'NF5503 Label Test',
      align: Nf5503PrintAlign.center,
      fontSize: Nf5503PrintFontSize.middle,
      isBold: true,
    );
    await _plugin.printer.addText(
      DateTime.now().toIso8601String(),
      align: Nf5503PrintAlign.center,
    );
    await _plugin.printer.goToNextMark();
    setState(() => _printerOpen = true);
    _log('已提交标签打印样张并走到下一黑标');
  }

  Future<void> _closePrinter() async {
    final success = await _plugin.printer.close();
    setState(() {
      _printerOpen = false;
      _printServiceEnabled = false;
    });
    _log('closePrinter 返回: $success');
  }

  Future<void> _waitForPrinterIdle({
    Duration timeout = const Duration(seconds: 8),
  }) async {
    final deadline = DateTime.now().add(timeout);
    while (DateTime.now().isBefore(deadline)) {
      final state = await _plugin.printer.getState(
        Nf5503PrinterStateType.checkBusy,
      );
      if (state == Nf5503PrintErrorCode.noError.value) {
        return;
      }
      await Future<void>.delayed(const Duration(milliseconds: 350));
    }
    _log('等待打印机空闲超时,继续提交后续校准内容');
  }

  int _nativeDensityStep(int density) {
    final step = (density / 4).ceil();
    if (step < 1) {
      return 1;
    }
    if (step > 10) {
      return 10;
    }
    return step;
  }

  int _blackMarkThreshold() {
    final value =
        int.tryParse(_blackMarkThresholdController.text.trim()) ?? 120;
    return value.clamp(0, 255).toInt();
  }

  String _paperTypeLabel(_PrintPaperType type) {
    return type == _PrintPaperType.label ? '热敏标签纸' : '热敏小票纸';
  }

  Future<Uint8List> _buildPrintImageBytes() async {
    const width = 384;
    const height = 160;
    final recorder = ui.PictureRecorder();
    final canvas = ui.Canvas(recorder);
    final whitePaint = ui.Paint()..color = const ui.Color(0xFFFFFFFF);
    final blackPaint = ui.Paint()..color = const ui.Color(0xFF000000);
    final borderPaint = ui.Paint()
      ..color = const ui.Color(0xFF000000)
      ..style = ui.PaintingStyle.stroke
      ..strokeWidth = 4;

    canvas.drawRect(
      ui.Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
      whitePaint,
    );
    canvas.drawRect(
      ui.Rect.fromLTWH(
        12,
        12,
        (width - 24).toDouble(),
        (height - 24).toDouble(),
      ),
      borderPaint,
    );

    final title = TextPainter(
      text: const TextSpan(
        text: 'NF5503 IMAGE TEST',
        style: TextStyle(
          color: Color(0xFF000000),
          fontSize: 28,
          fontWeight: FontWeight.w800,
        ),
      ),
      textDirection: TextDirection.ltr,
    )..layout(maxWidth: width - 48);
    title.paint(canvas, const ui.Offset(24, 28));

    for (var index = 0; index < 9; index++) {
      final left = 24.0 + index * 38;
      final top = index.isEven ? 86.0 : 104.0;
      canvas.drawRect(ui.Rect.fromLTWH(left, top, 26, 26), blackPaint);
    }

    final picture = recorder.endRecording();
    final image = await picture.toImage(width, height);
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    return byteData!.buffer.asUint8List();
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoApp(
      debugShowCheckedModeBanner: false,
      title: 'NF5503 Flutter Example',
      theme: const CupertinoThemeData(
        brightness: Brightness.dark,
        primaryColor: _AppColors.primary,
        scaffoldBackgroundColor: _AppColors.paper,
        textTheme: CupertinoTextThemeData(
          textStyle: TextStyle(
            color: _AppColors.ink,
            fontSize: 15,
            height: 1.35,
            fontFamily: '.SF Pro Text',
          ),
        ),
      ),
      home: CupertinoPageScaffold(
        child: DecoratedBox(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
              colors: [_AppColors.ink, _AppColors.deepTeal, _AppColors.paper],
            ),
          ),
          child: SafeArea(
            child: CustomScrollView(
              slivers: [
                SliverPadding(
                  padding: const EdgeInsets.fromLTRB(18, 18, 18, 28),
                  sliver: SliverList.list(
                    children: [
                      _Header(platformVersion: _platformVersion, busy: _busy),
                      const SizedBox(height: 16),
                      _scannerCard(),
                      const SizedBox(height: 14),
                      _printerCard(),
                      const SizedBox(height: 14),
                      _LogCard(
                        logs: _logs,
                        onClear: () => setState(_logs.clear),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _scannerCard() {
    return _SectionCard(
      icon: CupertinoIcons.barcode_viewfinder,
      title: '扫码 SDK 测试',
      subtitle: '先初始化广播并监听,再点击开始扫码或按设备扫码键。',
      children: [
        Row(
          children: [
            Expanded(
              child: _InfoTile(
                label: '扫描头',
                value: _scannerOpen == true ? 'Open' : 'Closed',
              ),
            ),
            const SizedBox(width: 10),
            Expanded(
              child: _InfoTile(label: '输出模式', value: _scannerMode),
            ),
          ],
        ),
        const SizedBox(height: 12),
        _CupertinoField(
          label: '广播 Action',
          controller: _scanActionController,
          enabled: !_busy,
        ),
        const SizedBox(height: 10),
        _CupertinoField(
          label: '广播 Key',
          controller: _scanKeyController,
          enabled: !_busy,
        ),
        const SizedBox(height: 12),
        _ResultBox(title: '扫码内容', value: _lastScan, result: _lastScanResult),
        if (_scanRecords.isNotEmpty) ...[
          const SizedBox(height: 10),
          _ScanHistoryPanel(records: _scanRecords),
        ],
        const SizedBox(height: 12),
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: [
            _ActionButton(
              label: '初始化/监听',
              icon: CupertinoIcons.antenna_radiowaves_left_right,
              tone: _ActionTone.filled,
              onPressed: _busy ? null : () => _run('初始化扫码并监听', _prepareScanner),
            ),
            _ActionButton(
              label: '开始扫码',
              icon: CupertinoIcons.play_arrow_solid,
              onPressed: _busy ? null : () => _run('开始扫码', _startDecode),
            ),
            _ActionButton(
              label: '停止',
              icon: CupertinoIcons.stop_fill,
              tone: _ActionTone.plain,
              onPressed: _busy ? null : () => _run('停止扫码', _stopDecode),
            ),
            _ActionButton(
              label: '读配置',
              icon: CupertinoIcons.doc_text_search,
              tone: _ActionTone.plain,
              onPressed: _busy
                  ? null
                  : () => _run('读取扫码配置', _readScannerConfig),
            ),
            _ActionButton(
              label: '关闭',
              icon: CupertinoIcons.power,
              tone: _ActionTone.plain,
              onPressed: _busy ? null : () => _run('关闭扫码', _closeScanner),
            ),
          ],
        ),
      ],
    );
  }

  Widget _printerCard() {
    final state = _lastPrinterState;
    final code = state == null
        ? '-'
        : (Nf5503PrintErrorCode.fromValue(state)?.name ?? '$state');
    final isLabelPaper = _printPaperType == _PrintPaperType.label;
    return _SectionCard(
      icon: CupertinoIcons.printer,
      title: '打印 SDK 测试',
      subtitle: '按官方 demo 的打印服务配置组织:通用设置、标签校准和打印测试项。',
      children: [
        Row(
          children: [
            Expanded(
              child: _InfoTile(
                label: '打印机',
                value: _printerOpen == true ? 'Open' : 'Closed',
              ),
            ),
            const SizedBox(width: 10),
            Expanded(
              child: _InfoTile(label: '支持模块', value: '${_supportPrint ?? '-'}'),
            ),
          ],
        ),
        const SizedBox(height: 10),
        Row(
          children: [
            Expanded(
              child: _InfoTile(label: '版本', value: _printerVersion),
            ),
            const SizedBox(width: 10),
            Expanded(
              child: _InfoTile(label: '状态', value: code),
            ),
          ],
        ),
        const SizedBox(height: 12),
        _PrintServiceSwitchPanel(
          value: _printServiceEnabled,
          enabled: !_busy,
          onChanged: (value) => _run(
            value ? '开启打印服务' : '关闭打印服务',
            () => _setPrintServiceEnabled(value),
          ),
        ),
        const SizedBox(height: 12),
        _PrinterCommonSettingsPanel(
          paperType: _printPaperType,
          density: _thermalCalibrationDensity,
          fontName: _printFontName,
          enabled: !_busy,
          onPaperTypeChanged: (type) =>
              _run('设置打印纸类型', () => _setPaperType(type)),
          onDensityChanged: (value) {
            setState(() => _thermalCalibrationDensity = value);
          },
          onDensityChangeEnd: (value) =>
              _run('设置打印浓度', () => _setPrintDensity(value)),
          onFontTap: () => _run('读取打印字体', _selectPrintFont),
        ),
        const SizedBox(height: 12),
        if (isLabelPaper) ...[
          _LabelCalibrationPanel(
            enabled: !_busy,
            thresholdEnabled: _blackMarkThresholdEnabled,
            thresholdController: _blackMarkThresholdController,
            onThresholdEnabledChanged: (value) =>
                _run('设置黑标阈值启用开关', () => _setBlackMarkThresholdEnabled(value)),
            onGetThreshold: () => _run('获取黑标阈值', _getBlackMarkThreshold),
            onSetThreshold: () => _run('设置黑标阈值', _setBlackMarkThreshold),
            onPrintTest: () => _run('黑标阈值打印测试', _printBlackMarkTest),
            onAutoCalibrate: () => _run('自动校准黑标', _autoCalibrateBlackMark),
          ),
          const SizedBox(height: 12),
        ],
        _PrintTestItemsPanel(
          enabled: !_busy,
          onTextPrint: () => _run('文本打印', _printTextTest),
          onBarcodePrint: () => _run('条码打印', _printBarcodeTest),
          onImagePrint: () => _run('图片打印', _printImageTest),
          onLabelPrint: () => _run('标签打印', _printLabelSample),
          onCalibrationScale: () =>
              _run('热敏浓度校准尺', _printThermalCalibrationScale),
        ),
      ],
    );
  }
}

class _AppColors {
  const _AppColors._();

  static const ink = Color(0xFF081816);
  static const deepTeal = Color(0xFF123D37);
  static const primary = Color(0xFF087568);
  static const accent = Color(0xFFF6C453);
  static const paper = Color(0xFFFFF5DC);
  static const card = Color(0xFFFFFCF5);
  static const mist = Color(0xFFE7F3EF);
  static const line = Color(0xFFE7DED0);
  static const panel = Color(0xFFF7F7F7);
  static const blue = Color(0xFF0A84FF);
  static const white = Color(0xFFFFFFFF);
  static const white70 = Color(0xB3FFFFFF);
}

class _Header extends StatelessWidget {
  const _Header({required this.platformVersion, required this.busy});

  final String platformVersion;
  final bool busy;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: _AppColors.white.withValues(alpha: 0.12),
        borderRadius: BorderRadius.circular(28),
        border: Border.all(color: _AppColors.white.withValues(alpha: 0.24)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              const _IconBadge(
                icon: CupertinoIcons.waveform_path_ecg,
                backgroundColor: _AppColors.accent,
                foregroundColor: _AppColors.ink,
                size: 48,
              ),
              const SizedBox(width: 12),
              const Expanded(
                child: Text(
                  'NF5503 SDK Probe',
                  style: TextStyle(
                    color: _AppColors.white,
                    fontSize: 26,
                    fontWeight: FontWeight.w800,
                    letterSpacing: -0.8,
                    height: 1.05,
                  ),
                ),
              ),
              if (busy)
                const SizedBox(
                  width: 24,
                  height: 24,
                  child: CupertinoActivityIndicator(color: _AppColors.white),
                ),
            ],
          ),
          const SizedBox(height: 10),
          Text(
            '用于真机验证扫码与打印官方 SDK 是否能被 Flutter 插件正确调用。',
            style: TextStyle(
              color: _AppColors.white.withValues(alpha: 0.82),
              fontSize: 14,
              height: 1.45,
            ),
          ),
          const SizedBox(height: 14),
          _GlassPill(label: 'Platform', value: platformVersion),
        ],
      ),
    );
  }
}

class _SectionCard extends StatelessWidget {
  const _SectionCard({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.children,
  });

  final IconData icon;
  final String title;
  final String subtitle;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: _AppColors.card.withValues(alpha: 0.96),
        borderRadius: BorderRadius.circular(24),
        boxShadow: [
          BoxShadow(
            blurRadius: 24,
            offset: const Offset(0, 12),
            color: _AppColors.ink.withValues(alpha: 0.16),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              _IconBadge(icon: icon),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      title,
                      style: const TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.w800,
                        color: _AppColors.ink,
                        letterSpacing: -0.2,
                      ),
                    ),
                    const SizedBox(height: 2),
                    Text(
                      subtitle,
                      style: const TextStyle(
                        color: Color(0xFF55615E),
                        fontSize: 13,
                        height: 1.35,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 16),
          ...children,
        ],
      ),
    );
  }
}

class _IconBadge extends StatelessWidget {
  const _IconBadge({
    required this.icon,
    this.backgroundColor = _AppColors.primary,
    this.foregroundColor = _AppColors.white,
    this.size = 42,
  });

  final IconData icon;
  final Color backgroundColor;
  final Color foregroundColor;
  final double size;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
      child: Icon(icon, color: foregroundColor, size: size * 0.52),
    );
  }
}

class _PrintServiceSwitchPanel extends StatelessWidget {
  const _PrintServiceSwitchPanel({
    required this.value,
    required this.enabled,
    required this.onChanged,
  });

  final bool value;
  final bool enabled;
  final ValueChanged<bool> onChanged;

  @override
  Widget build(BuildContext context) {
    return _OfficialPanel(
      child: Row(
        children: [
          const Expanded(
            child: Text(
              '打印服务开关',
              style: TextStyle(
                color: Color(0xFF1F2933),
                fontWeight: FontWeight.w700,
                fontSize: 16,
              ),
            ),
          ),
          CupertinoSwitch(
            value: value,
            activeTrackColor: _AppColors.primary,
            onChanged: enabled ? onChanged : null,
          ),
        ],
      ),
    );
  }
}

class _PrinterCommonSettingsPanel extends StatelessWidget {
  const _PrinterCommonSettingsPanel({
    required this.paperType,
    required this.density,
    required this.fontName,
    required this.enabled,
    required this.onPaperTypeChanged,
    required this.onDensityChanged,
    required this.onDensityChangeEnd,
    required this.onFontTap,
  });

  final _PrintPaperType paperType;
  final int density;
  final String fontName;
  final bool enabled;
  final ValueChanged<_PrintPaperType> onPaperTypeChanged;
  final ValueChanged<int> onDensityChanged;
  final ValueChanged<int> onDensityChangeEnd;
  final VoidCallback onFontTap;

  @override
  Widget build(BuildContext context) {
    return _OfficialPanel(
      child: Column(
        children: [
          const _OfficialPanelHeader(
            icon: CupertinoIcons.slider_horizontal_3,
            title: '通用设置',
          ),
          const SizedBox(height: 14),
          _ConfigLine(
            label: '打印纸类型:',
            child: Wrap(
              spacing: 10,
              runSpacing: 8,
              children: [
                _PaperRadio(
                  title: '热敏小票纸',
                  value: _PrintPaperType.receipt,
                  groupValue: paperType,
                  enabled: enabled,
                  onChanged: onPaperTypeChanged,
                ),
                _PaperRadio(
                  title: '热敏标签纸',
                  value: _PrintPaperType.label,
                  groupValue: paperType,
                  enabled: enabled,
                  onChanged: onPaperTypeChanged,
                ),
              ],
            ),
          ),
          const SizedBox(height: 12),
          _ConfigLine(
            label: '打印机尺寸:',
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: const [
                _RadioDot(selected: true, enabled: true),
                SizedBox(width: 8),
                Text('2寸(58mm)'),
              ],
            ),
          ),
          const SizedBox(height: 12),
          _ConfigLine(
            label: '当前打印浓度:',
            child: Row(
              children: [
                Expanded(
                  child: CupertinoSlider(
                    value: density.toDouble(),
                    min: 1,
                    max: 40,
                    divisions: 39,
                    activeColor: _AppColors.primary,
                    onChanged: enabled
                        ? (value) => onDensityChanged(value.round())
                        : null,
                    onChangeEnd: enabled
                        ? (value) => onDensityChangeEnd(value.round())
                        : null,
                  ),
                ),
                SizedBox(
                  width: 34,
                  child: Text(
                    '$density',
                    textAlign: TextAlign.right,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 12),
          _Pressable(
            enabled: enabled,
            onTap: onFontTap,
            borderRadius: BorderRadius.circular(10),
            child: _ConfigLine(
              label: '打印字体:',
              child: Row(
                children: [
                  Expanded(child: Text(fontName)),
                  const Icon(
                    CupertinoIcons.chevron_right,
                    color: _AppColors.blue,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _LabelCalibrationPanel extends StatelessWidget {
  const _LabelCalibrationPanel({
    required this.enabled,
    required this.thresholdEnabled,
    required this.thresholdController,
    required this.onThresholdEnabledChanged,
    required this.onGetThreshold,
    required this.onSetThreshold,
    required this.onPrintTest,
    required this.onAutoCalibrate,
  });

  final bool enabled;
  final bool thresholdEnabled;
  final TextEditingController thresholdController;
  final ValueChanged<bool> onThresholdEnabledChanged;
  final VoidCallback onGetThreshold;
  final VoidCallback onSetThreshold;
  final VoidCallback onPrintTest;
  final VoidCallback onAutoCalibrate;

  @override
  Widget build(BuildContext context) {
    return _OfficialPanel(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Row(
            children: [
              Icon(CupertinoIcons.sparkles, color: _AppColors.blue),
              SizedBox(width: 12),
              Text(
                '标签校准',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
              ),
              SizedBox(width: 8),
              Icon(CupertinoIcons.question_circle, color: _AppColors.blue),
            ],
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              const Expanded(child: Text('黑标阈值启用开关')),
              CupertinoSwitch(
                value: thresholdEnabled,
                activeTrackColor: _AppColors.primary,
                onChanged: enabled ? onThresholdEnabledChanged : null,
              ),
            ],
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 10,
            runSpacing: 10,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: [
              const Text('黑标阈值'),
              SizedBox(
                width: 94,
                child: CupertinoTextField(
                  controller: thresholdController,
                  enabled: enabled && thresholdEnabled,
                  keyboardType: TextInputType.number,
                  textAlign: TextAlign.center,
                  inputFormatters: [FilteringTextInputFormatter.digitsOnly],
                  padding: const EdgeInsets.symmetric(
                    horizontal: 10,
                    vertical: 10,
                  ),
                  decoration: BoxDecoration(
                    color: _AppColors.white,
                    border: Border.all(color: _AppColors.line),
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),
              ),
              _OfficialButton(
                label: '获取',
                onPressed: enabled ? onGetThreshold : null,
              ),
              _OfficialButton(
                label: '设置',
                onPressed: enabled && thresholdEnabled ? onSetThreshold : null,
              ),
              _OfficialButton(
                label: '打印测试',
                onPressed: enabled ? onPrintTest : null,
              ),
              _OfficialButton(
                label: '自动校准',
                onPressed: enabled && thresholdEnabled ? onAutoCalibrate : null,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _PrintTestItemsPanel extends StatelessWidget {
  const _PrintTestItemsPanel({
    required this.enabled,
    required this.onTextPrint,
    required this.onBarcodePrint,
    required this.onImagePrint,
    required this.onLabelPrint,
    required this.onCalibrationScale,
  });

  final bool enabled;
  final VoidCallback onTextPrint;
  final VoidCallback onBarcodePrint;
  final VoidCallback onImagePrint;
  final VoidCallback onLabelPrint;
  final VoidCallback onCalibrationScale;

  @override
  Widget build(BuildContext context) {
    return _OfficialPanel(
      padding: EdgeInsets.zero,
      child: Column(
        children: [
          const Padding(
            padding: EdgeInsets.fromLTRB(16, 16, 16, 10),
            child: _OfficialPanelHeader(
              icon: CupertinoIcons.printer_fill,
              title: '打印测试项',
            ),
          ),
          _PrintTestTile(
            icon: CupertinoIcons.text_alignleft,
            title: '文本打印',
            enabled: enabled,
            onTap: onTextPrint,
          ),
          _PrintTestTile(
            icon: CupertinoIcons.barcode,
            title: '条码打印',
            enabled: enabled,
            onTap: onBarcodePrint,
          ),
          _PrintTestTile(
            icon: CupertinoIcons.photo,
            title: '图片打印',
            enabled: enabled,
            onTap: onImagePrint,
          ),
          _PrintTestTile(
            icon: CupertinoIcons.tag,
            title: '标签打印',
            enabled: enabled,
            onTap: onLabelPrint,
          ),
          _PrintTestTile(
            icon: CupertinoIcons.chart_bar,
            title: '热敏浓度尺',
            enabled: enabled,
            onTap: onCalibrationScale,
            showDivider: false,
          ),
        ],
      ),
    );
  }
}

class _OfficialPanel extends StatelessWidget {
  const _OfficialPanel({
    required this.child,
    this.padding = const EdgeInsets.all(16),
  });

  final Widget child;
  final EdgeInsetsGeometry padding;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: padding,
      decoration: BoxDecoration(
        color: _AppColors.panel,
        borderRadius: BorderRadius.circular(14),
        border: Border.all(color: _AppColors.line),
      ),
      child: child,
    );
  }
}

class _OfficialPanelHeader extends StatelessWidget {
  const _OfficialPanelHeader({required this.icon, required this.title});

  final IconData icon;
  final String title;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Icon(icon, color: _AppColors.blue),
        const SizedBox(width: 12),
        Text(
          title,
          style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
        ),
      ],
    );
  }
}

class _ConfigLine extends StatelessWidget {
  const _ConfigLine({required this.label, required this.child});

  final String label;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final narrow = constraints.maxWidth < 360;
        if (narrow) {
          return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
              const SizedBox(height: 8),
              child,
            ],
          );
        }
        return Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            SizedBox(
              width: 112,
              child: Text(
                label,
                style: const TextStyle(fontWeight: FontWeight.w600),
              ),
            ),
            Expanded(child: child),
          ],
        );
      },
    );
  }
}

class _PaperRadio extends StatelessWidget {
  const _PaperRadio({
    required this.title,
    required this.value,
    required this.groupValue,
    required this.enabled,
    required this.onChanged,
  });

  final String title;
  final _PrintPaperType value;
  final _PrintPaperType groupValue;
  final bool enabled;
  final ValueChanged<_PrintPaperType> onChanged;

  @override
  Widget build(BuildContext context) {
    return _Pressable(
      enabled: enabled,
      onTap: () => onChanged(value),
      borderRadius: BorderRadius.circular(999),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 4),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            _RadioDot(selected: value == groupValue, enabled: enabled),
            const SizedBox(width: 8),
            Text(title),
          ],
        ),
      ),
    );
  }
}

class _RadioDot extends StatelessWidget {
  const _RadioDot({required this.selected, required this.enabled});

  final bool selected;
  final bool enabled;

  @override
  Widget build(BuildContext context) {
    final active = enabled ? _AppColors.blue : const Color(0xFF9CA3AF);
    return Container(
      width: 22,
      height: 22,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(
          color: selected ? active : const Color(0xFF111827),
          width: 2,
        ),
      ),
      child: selected
          ? Center(
              child: Container(
                width: 10,
                height: 10,
                decoration: BoxDecoration(
                  color: active,
                  shape: BoxShape.circle,
                ),
              ),
            )
          : null,
    );
  }
}

class _OfficialButton extends StatelessWidget {
  const _OfficialButton({required this.label, required this.onPressed});

  final String label;
  final VoidCallback? onPressed;

  @override
  Widget build(BuildContext context) {
    return CupertinoButton(
      onPressed: onPressed,
      color: _AppColors.blue,
      disabledColor: const Color(0xFFB6C8D5),
      borderRadius: BorderRadius.circular(10),
      padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
      minimumSize: const Size(0, 38),
      child: Text(label, style: const TextStyle(color: _AppColors.white)),
    );
  }
}

class _PrintTestTile extends StatelessWidget {
  const _PrintTestTile({
    required this.icon,
    required this.title,
    required this.enabled,
    required this.onTap,
    this.showDivider = true,
  });

  final IconData icon;
  final String title;
  final bool enabled;
  final VoidCallback onTap;
  final bool showDivider;

  @override
  Widget build(BuildContext context) {
    final content = _Pressable(
      enabled: enabled,
      onTap: onTap,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
        child: Row(
          children: [
            Icon(
              icon,
              color: enabled ? _AppColors.primary : const Color(0xFF9CA3AF),
              size: 22,
            ),
            const SizedBox(width: 10),
            Expanded(
              child: Text(
                title,
                style: TextStyle(
                  color: enabled ? _AppColors.ink : const Color(0xFF9CA3AF),
                  fontSize: 16,
                ),
              ),
            ),
            const Icon(
              CupertinoIcons.chevron_right,
              color: _AppColors.blue,
              size: 18,
            ),
          ],
        ),
      ),
    );
    if (!showDivider) {
      return content;
    }
    return Column(children: [content, const _Hairline()]);
  }
}

class _CupertinoField extends StatelessWidget {
  const _CupertinoField({
    required this.label,
    required this.controller,
    required this.enabled,
  });

  final String label;
  final TextEditingController controller;
  final bool enabled;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: const TextStyle(
            fontSize: 13,
            fontWeight: FontWeight.w700,
            color: Color(0xFF55615E),
          ),
        ),
        const SizedBox(height: 6),
        CupertinoTextField(
          controller: controller,
          enabled: enabled,
          clearButtonMode: OverlayVisibilityMode.editing,
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
          decoration: BoxDecoration(
            color: _AppColors.white,
            borderRadius: BorderRadius.circular(14),
            border: Border.all(color: _AppColors.line),
          ),
          style: const TextStyle(fontSize: 14, color: _AppColors.ink),
        ),
      ],
    );
  }
}

class _InfoTile extends StatelessWidget {
  const _InfoTile({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: _AppColors.mist,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            label,
            style: const TextStyle(
              color: Color(0xFF596863),
              fontSize: 12,
              fontWeight: FontWeight.w600,
            ),
          ),
          const SizedBox(height: 4),
          Text(
            value,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: const TextStyle(
              color: _AppColors.ink,
              fontSize: 16,
              fontWeight: FontWeight.w800,
            ),
          ),
        ],
      ),
    );
  }
}

class _ResultBox extends StatelessWidget {
  const _ResultBox({required this.title, required this.value, this.result});

  final String title;
  final String value;
  final Nf5503ScanResult? result;

  @override
  Widget build(BuildContext context) {
    final extras = result?.extras ?? const <String, Object?>{};
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: _AppColors.ink,
        borderRadius: BorderRadius.circular(18),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            title,
            style: const TextStyle(
              color: _AppColors.accent,
              fontWeight: FontWeight.w700,
            ),
          ),
          const SizedBox(height: 8),
          Text(
            value,
            style: const TextStyle(
              color: _AppColors.white,
              fontSize: 18,
              fontWeight: FontWeight.w700,
              height: 1.35,
            ),
          ),
          if (result != null) ...[
            const SizedBox(height: 10),
            _ScanMetaLine(label: 'Action', value: result!.action ?? '-'),
            _ScanMetaLine(label: 'Extras', value: _formatExtras(extras)),
          ],
        ],
      ),
    );
  }

  static String _formatExtras(Map<String, Object?> extras) {
    if (extras.isEmpty) {
      return '-';
    }
    return extras.entries
        .map((entry) => '${entry.key}=${entry.value ?? 'null'}')
        .join(', ');
  }
}

class _ScanMetaLine extends StatelessWidget {
  const _ScanMetaLine({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Text(
      '$label: $value',
      maxLines: 2,
      overflow: TextOverflow.ellipsis,
      style: TextStyle(
        color: _AppColors.white.withValues(alpha: 0.72),
        fontSize: 12,
        height: 1.3,
      ),
    );
  }
}

class _ScanHistoryPanel extends StatelessWidget {
  const _ScanHistoryPanel({required this.records});

  final List<_ScanRecord> records;

  @override
  Widget build(BuildContext context) {
    return _OfficialPanel(
      padding: EdgeInsets.zero,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Padding(
            padding: EdgeInsets.fromLTRB(14, 12, 14, 8),
            child: Text(
              '扫码记录',
              style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800),
            ),
          ),
          for (final record in records.take(5)) ...[
            _ScanHistoryTile(record: record),
            if (record != records.take(5).last) const _Hairline(),
          ],
        ],
      ),
    );
  }
}

class _ScanHistoryTile extends StatelessWidget {
  const _ScanHistoryTile({required this.record});

  final _ScanRecord record;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            decoration: BoxDecoration(
              color: _AppColors.mist,
              borderRadius: BorderRadius.circular(999),
            ),
            child: Text(
              record.time,
              style: const TextStyle(
                color: _AppColors.primary,
                fontSize: 12,
                fontWeight: FontWeight.w800,
              ),
            ),
          ),
          const SizedBox(width: 10),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  record.content,
                  style: const TextStyle(
                    color: _AppColors.ink,
                    fontSize: 15,
                    fontWeight: FontWeight.w700,
                  ),
                ),
                if (record.action != null || record.extras.isNotEmpty) ...[
                  const SizedBox(height: 3),
                  Text(
                    record.action ?? _ResultBox._formatExtras(record.extras),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    style: const TextStyle(
                      color: Color(0xFF6B7280),
                      fontSize: 12,
                    ),
                  ),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _GlassPill extends StatelessWidget {
  const _GlassPill({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: _AppColors.white.withValues(alpha: 0.16),
        borderRadius: BorderRadius.circular(999),
      ),
      child: Text(
        '$label  $value',
        style: const TextStyle(
          color: _AppColors.white,
          fontWeight: FontWeight.w700,
        ),
      ),
    );
  }
}

class _LogCard extends StatelessWidget {
  const _LogCard({required this.logs, required this.onClear});

  final List<String> logs;
  final VoidCallback onClear;

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      icon: CupertinoIcons.rectangle_stack,
      title: '调用日志',
      subtitle: '每一步 MethodChannel/EventChannel 调用结果都会记录在这里。',
      children: [
        Align(
          alignment: Alignment.centerRight,
          child: _ActionButton(
            label: '清空',
            icon: CupertinoIcons.trash,
            tone: _ActionTone.plain,
            onPressed: onClear,
          ),
        ),
        Container(
          constraints: const BoxConstraints(minHeight: 180, maxHeight: 320),
          width: double.infinity,
          padding: const EdgeInsets.all(12),
          decoration: BoxDecoration(
            color: _AppColors.ink,
            borderRadius: BorderRadius.circular(18),
          ),
          child: logs.isEmpty
              ? const Text('暂无日志', style: TextStyle(color: _AppColors.white70))
              : ListView.separated(
                  shrinkWrap: true,
                  itemCount: logs.length,
                  separatorBuilder: (_, _) => const SizedBox(height: 8),
                  itemBuilder: (context, index) => Text(
                    logs[index],
                    style: const TextStyle(
                      color: Color(0xFFD1FAE5),
                      fontSize: 12,
                      height: 1.25,
                    ),
                  ),
                ),
        ),
      ],
    );
  }
}

class _ActionButton extends StatelessWidget {
  const _ActionButton({
    required this.label,
    required this.icon,
    required this.onPressed,
    this.tone = _ActionTone.tinted,
  });

  final String label;
  final IconData icon;
  final VoidCallback? onPressed;
  final _ActionTone tone;

  @override
  Widget build(BuildContext context) {
    final child = Row(
      mainAxisSize: MainAxisSize.min,
      children: [Icon(icon, size: 18), const SizedBox(width: 6), Text(label)],
    );

    return switch (tone) {
      _ActionTone.filled => CupertinoButton.filled(
        onPressed: onPressed,
        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
        minimumSize: const Size(0, 40),
        borderRadius: BorderRadius.circular(12),
        child: child,
      ),
      _ActionTone.tinted => CupertinoButton.tinted(
        onPressed: onPressed,
        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
        minimumSize: const Size(0, 40),
        borderRadius: BorderRadius.circular(12),
        child: child,
      ),
      _ActionTone.plain => CupertinoButton(
        onPressed: onPressed,
        color: _AppColors.white,
        foregroundColor: _AppColors.primary,
        disabledColor: const Color(0xFFE5E7EB),
        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
        minimumSize: const Size(0, 40),
        borderRadius: BorderRadius.circular(12),
        child: child,
      ),
    };
  }
}

class _Pressable extends StatefulWidget {
  const _Pressable({
    required this.child,
    this.enabled = true,
    this.onTap,
    this.borderRadius,
  });

  final Widget child;
  final bool enabled;
  final VoidCallback? onTap;
  final BorderRadius? borderRadius;

  @override
  State<_Pressable> createState() => _PressableState();
}

class _PressableState extends State<_Pressable> {
  bool _pressed = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: widget.enabled ? widget.onTap : null,
      onTapDown: widget.enabled ? (_) => setState(() => _pressed = true) : null,
      onTapCancel: widget.enabled
          ? () => setState(() => _pressed = false)
          : null,
      onTapUp: widget.enabled ? (_) => setState(() => _pressed = false) : null,
      child: AnimatedOpacity(
        duration: const Duration(milliseconds: 120),
        opacity: _pressed ? 0.62 : (widget.enabled ? 1 : 0.48),
        child: DecoratedBox(
          decoration: BoxDecoration(borderRadius: widget.borderRadius),
          child: widget.child,
        ),
      ),
    );
  }
}

class _Hairline extends StatelessWidget {
  const _Hairline();

  @override
  Widget build(BuildContext context) {
    return Container(height: 1, color: _AppColors.line);
  }
}
0
likes
150
points
9
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

M5Stack NF5503 Android barcode scanner and thermal/label printer Flutter plugin.

Repository (GitHub)
View/report issues

Topics

#android #barcode #printer #thermal-printer #m5stack

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on nf5503_flutter

Packages that implement nf5503_flutter