printer_tsc 0.0.4
printer_tsc: ^0.0.4 copied to clipboard
A Flutter plugin for TSC printers.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:printer_tsc/printer_tsc.dart';
import 'package:printer_tsc/printer_tsc_platform_interface.dart';
void main() {
runApp(const PrinterConnectionTestApp());
}
class PrinterConnectionTestApp extends StatelessWidget {
const PrinterConnectionTestApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF0B6E4F),
brightness: Brightness.light,
),
scaffoldBackgroundColor: const Color(0xFFF4F1E8),
inputDecorationTheme: const InputDecorationTheme(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(18)),
),
),
),
home: const PrinterConnectionTestPage(),
);
}
}
enum _FeedbackTone {
idle,
loading,
success,
error,
}
class PrinterConnectionTestPage extends StatefulWidget {
const PrinterConnectionTestPage({super.key});
@override
State<PrinterConnectionTestPage> createState() => _PrinterConnectionTestPageState();
}
class _PrinterConnectionTestPageState extends State<PrinterConnectionTestPage> {
final PrinterTsc _printer = PrinterTsc();
final TextEditingController _bluetoothMacController =
TextEditingController(text: '00:19:0E:A0:04:E1');
final TextEditingController _ethernetIpController =
TextEditingController(text: '192.168.1.50');
final TextEditingController _ethernetPortController =
TextEditingController(text: '9100');
final TextEditingController _usbTimeoutController =
TextEditingController(text: '3000');
final TextEditingController _windowsPortController =
TextEditingController(text: 'TSC MA2400');
final TextEditingController _windowsFontXController =
TextEditingController(text: '40');
final TextEditingController _windowsFontYController =
TextEditingController(text: '40');
final TextEditingController _windowsFontHeightController =
TextEditingController(text: '32');
final TextEditingController _windowsFontRotationController =
TextEditingController(text: '0');
final TextEditingController _windowsFontStyleController =
TextEditingController(text: '0');
final TextEditingController _windowsFontUnderlineController =
TextEditingController(text: '0');
final TextEditingController _windowsFontFaceController =
TextEditingController(text: '黑体');
final TextEditingController _windowsFontContentController =
TextEditingController(text: '测试中文Abc123');
final TextEditingController _tsplCommandController = TextEditingController(
text:
'CLS\r\nTEXT 40,40,"3",0,1,1,"printer_tsc"\r\nPRINT 1,1\r\nCLS\r\n',
);
String _platformVersion = 'Loading...';
String _activeConnectionLabel = 'Not connected';
String _lastStatus = '-';
String _lastBatteryInfo = '-';
int _connectionTabIndex = 0;
String _feedbackTitle = 'Ready';
String _feedbackDetail = 'Select a connection type and start testing.';
_FeedbackTone _feedbackTone = _FeedbackTone.loading;
bool _busy = false;
@override
void initState() {
super.initState();
_loadPlatformVersion();
}
@override
void dispose() {
_bluetoothMacController.dispose();
_ethernetIpController.dispose();
_ethernetPortController.dispose();
_usbTimeoutController.dispose();
_windowsPortController.dispose();
_windowsFontXController.dispose();
_windowsFontYController.dispose();
_windowsFontHeightController.dispose();
_windowsFontRotationController.dispose();
_windowsFontStyleController.dispose();
_windowsFontUnderlineController.dispose();
_windowsFontFaceController.dispose();
_windowsFontContentController.dispose();
_tsplCommandController.dispose();
super.dispose();
}
Future<void> _loadPlatformVersion() async {
try {
final String version =
await _printer.getPlatformVersion() ?? 'Unknown platform version';
if (!mounted) {
return;
}
setState(() {
_platformVersion = version;
});
} on PlatformException catch (exception) {
if (!mounted) {
return;
}
setState(() {
_platformVersion = 'Failed: ${exception.message ?? exception.code}';
});
}
}
void _setFeedback(
_FeedbackTone tone,
String title,
String detail,
) {
if (!mounted) {
return;
}
setState(() {
_feedbackTone = tone;
_feedbackTitle = title;
_feedbackDetail = detail;
});
if (tone == _FeedbackTone.loading) {
return;
}
final Color backgroundColor;
switch (tone) {
case _FeedbackTone.success:
backgroundColor = const Color(0xFF166534);
case _FeedbackTone.error:
backgroundColor = const Color(0xFFB42318);
case _FeedbackTone.idle:
case _FeedbackTone.loading:
backgroundColor = const Color(0xFF334155);
}
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: backgroundColor,
content: Text('$title: $detail'),
),
);
}
String _describePlatformException(PlatformException exception) {
final String message = exception.message?.trim() ?? '';
if (message.isNotEmpty) {
return message;
}
return exception.code;
}
String _statusText(PrinterStatus? status) {
switch (status) {
case PrinterStatus.idle:
return 'Idle';
case PrinterStatus.headOpened:
return 'Head Opened';
case PrinterStatus.paperJam:
return 'Paper Jam';
case PrinterStatus.paperJamAndHeadOpened:
return 'Paper Jam + Head Opened';
case PrinterStatus.paperEmpty:
return 'Paper Empty';
case PrinterStatus.paperEmptyAndHeadOpened:
return 'Paper Empty + Head Opened';
case PrinterStatus.ribbonEmpty:
return 'Ribbon Empty';
case PrinterStatus.ribbonEmptyAndHeadOpened:
return 'Ribbon Empty + Head Opened';
case PrinterStatus.ribbonEmptyAndPaperJam:
return 'Ribbon Empty + Paper Jam';
case PrinterStatus.ribbonEmptyAndPaperJamAndHeadOpened:
return 'Ribbon Empty + Paper Jam + Head Opened';
case PrinterStatus.ribbonEmptyAndPaperEmpty:
return 'Ribbon Empty + Paper Empty';
case PrinterStatus.ribbonEmptyAndPaperEmptyAndHeadOpened:
return 'Ribbon Empty + Paper Empty + Head Opened';
case PrinterStatus.paused:
return 'Paused';
case PrinterStatus.printing:
return 'Printing';
case PrinterStatus.otherError:
return 'Other Error';
case PrinterStatus.unknown:
case null:
return 'Unknown';
}
}
Future<void> _runBoolAction({
required String pendingTitle,
required String pendingDetail,
required Future<bool> Function() action,
required String successTitle,
required String successDetail,
required String failureTitle,
required String failureDetail,
void Function(bool success)? onCompleted,
}) async {
if (_busy) {
return;
}
setState(() {
_busy = true;
});
_setFeedback(_FeedbackTone.loading, pendingTitle, pendingDetail);
try {
final bool success = await action();
onCompleted?.call(success);
_setFeedback(
success ? _FeedbackTone.success : _FeedbackTone.error,
success ? successTitle : failureTitle,
success ? successDetail : failureDetail,
);
} on PlatformException catch (exception) {
_setFeedback(
_FeedbackTone.error,
failureTitle,
_describePlatformException(exception),
);
} catch (exception) {
_setFeedback(
_FeedbackTone.error,
failureTitle,
exception.toString(),
);
} finally {
if (!mounted) {
return;
}
setState(() {
_busy = false;
});
}
}
Future<void> _connectBluetooth() async {
final String macAddress = _bluetoothMacController.text.trim();
await _runBoolAction(
pendingTitle: 'Connecting Bluetooth',
pendingDetail: 'Trying $macAddress',
action: () async {
final bool connected = await _printer.openBluetoothPort(macAddress);
if (!connected) {
return false;
}
return _initializePrinterAfterConnect();
},
successTitle: 'Bluetooth Connected',
successDetail: '$macAddress (initialized with setup 65x147)',
failureTitle: 'Bluetooth Failed',
failureDetail: 'Unable to open Bluetooth printer or setup failed.',
onCompleted: (bool success) {
setState(() {
_activeConnectionLabel = success
? 'Bluetooth: $macAddress'
: 'Bluetooth connection failed';
});
},
);
}
Future<void> _connectEthernet() async {
final String ipAddress = _ethernetIpController.text.trim();
final String port = _ethernetPortController.text.trim();
await _runBoolAction(
pendingTitle: 'Connecting Ethernet',
pendingDetail: 'Trying $ipAddress:$port',
action: () async {
final bool connected = await _printer.openEthernetPort(ipAddress, port);
if (!connected) {
return false;
}
return _initializePrinterAfterConnect();
},
successTitle: 'Ethernet Connected',
successDetail: '$ipAddress:$port (initialized with setup 65x147)',
failureTitle: 'Ethernet Failed',
failureDetail: 'Unable to open Ethernet printer or setup failed.',
onCompleted: (bool success) {
setState(() {
_activeConnectionLabel = success
? 'Ethernet: $ipAddress:$port'
: 'Ethernet connection failed';
});
},
);
}
Future<void> _connectUsb() async {
final int? timeoutMs = int.tryParse(_usbTimeoutController.text.trim());
await _runBoolAction(
pendingTitle: 'Connecting USB',
pendingDetail: 'Waiting for USB permission or device open result.',
action: () async {
final bool connected = await _printer.openUsbPort(timeoutMs: timeoutMs);
if (!connected) {
return false;
}
return _initializePrinterAfterConnect();
},
successTitle: 'USB Connected',
successDetail: 'Printer opened and initialized with setup 65x147.',
failureTitle: 'USB Failed',
failureDetail: 'USB open returned false or setup failed.',
onCompleted: (bool success) {
setState(() {
_activeConnectionLabel = success ? 'USB connected' : 'USB connection failed';
});
},
);
}
Future<void> _testWindowsFont() async {
final int? x = int.tryParse(_windowsFontXController.text.trim());
final int? y = int.tryParse(_windowsFontYController.text.trim());
final int? fontheight = int.tryParse(_windowsFontHeightController.text.trim());
final int? rotation = int.tryParse(_windowsFontRotationController.text.trim());
final int? fontstyle = int.tryParse(_windowsFontStyleController.text.trim());
final int? fontunderline = int.tryParse(_windowsFontUnderlineController.text.trim());
final String szFaceName = _windowsFontFaceController.text.trim();
final String content = _windowsFontContentController.text.trim();
if (x == null || y == null || fontheight == null || rotation == null ||
fontstyle == null || fontunderline == null ||
szFaceName.isEmpty || content.isEmpty) {
_setFeedback(
_FeedbackTone.error,
'Invalid Input',
'All windowsfont fields are required.',
);
return;
}
await _runBoolAction(
pendingTitle: 'windowsfont',
pendingDetail: 'Printing text with Windows font "$szFaceName".',
action: () async {
final bool cleared = await _printer.clearBuffer();
if (!cleared) return false;
final bool printed = await _printer.windowsfont(
x, y, fontheight, rotation, fontstyle, fontunderline, szFaceName, content,
);
if (!printed) return false;
return _printer.printlabel(1, 1);
},
successTitle: 'windowsfont Sent',
successDetail: 'Text printed with font "$szFaceName".',
failureTitle: 'windowsfont Failed',
failureDetail: 'One of the commands returned false.',
);
}
Future<void> _connectWindowsPort() async {
final String portName = _windowsPortController.text.trim();
await _runBoolAction(
pendingTitle: 'Connecting Windows Port',
pendingDetail: 'Trying $portName',
action: () async {
final bool connected = await _printer.openPortForWindows(portName);
if (!connected) return false;
return _initializePrinterAfterConnect();
},
successTitle: 'Windows Port Connected',
successDetail: '$portName (initialized with setup 65x147)',
failureTitle: 'Windows Port Failed',
failureDetail: 'Unable to open port or setup failed.',
onCompleted: (bool success) {
setState(() {
_activeConnectionLabel =
success ? 'Windows: $portName' : 'Windows port connection failed';
});
},
);
}
Future<bool> _initializePrinterAfterConnect() {
return _printer.setup(65, 147, 4, 12, LabelSensorType.blackMark, 3, 0);
}
Future<void> _closePort() async {
await _runBoolAction(
pendingTitle: 'Closing Port',
pendingDetail: 'Sending close command.',
action: _printer.close,
successTitle: 'Port Closed',
successDetail: 'Printer connection has been closed.',
failureTitle: 'Close Failed',
failureDetail: 'The printer did not close cleanly.',
onCompleted: (bool success) {
setState(() {
_activeConnectionLabel = success ? 'Not connected' : 'Close command failed';
});
},
);
}
Future<void> _queryStatus() async {
if (_busy) {
return;
}
setState(() {
_busy = true;
});
_setFeedback(_FeedbackTone.loading, 'Reading Status', 'Querying printer status.');
try {
final PrinterStatus? status = await _printer.printerstatus(500);
final String statusText = _statusText(status);
setState(() {
_lastStatus = statusText;
});
_setFeedback(_FeedbackTone.success, 'Status Ready', statusText);
} on PlatformException catch (exception) {
_setFeedback(
_FeedbackTone.error,
'Status Failed',
_describePlatformException(exception),
);
} catch (exception) {
_setFeedback(_FeedbackTone.error, 'Status Failed', exception.toString());
} finally {
if (!mounted) {
return;
}
setState(() {
_busy = false;
});
}
}
Future<void> _queryBattery() async {
if (_busy) {
return;
}
setState(() {
_busy = true;
});
_setFeedback(_FeedbackTone.loading, 'Reading Battery', 'Querying smart battery voltage.');
try {
final String? value = await _printer.smartbatteryStatus(
SmartBatteryStatusType.voltage,
);
final String batteryText = (value == null || value.isEmpty) ? 'No data' : value;
setState(() {
_lastBatteryInfo = batteryText;
});
_setFeedback(_FeedbackTone.success, 'Battery Ready', batteryText);
} on PlatformException catch (exception) {
_setFeedback(
_FeedbackTone.error,
'Battery Failed',
_describePlatformException(exception),
);
} catch (exception) {
_setFeedback(_FeedbackTone.error, 'Battery Failed', exception.toString());
} finally {
if (!mounted) {
return;
}
setState(() {
_busy = false;
});
}
}
Future<void> _sendRawCommand() async {
await _runBoolAction(
pendingTitle: 'Sending Command',
pendingDetail: 'Dispatching TSPL command to the printer.',
action: () => _printer.sendcommand(_tsplCommandController.text),
successTitle: 'Command Sent',
successDetail: 'TSPL command was accepted.',
failureTitle: 'Command Failed',
failureDetail: 'TSPL command returned false.',
);
}
Future<void> _clearBufferOnly() async {
await _runBoolAction(
pendingTitle: 'Clearing Buffer',
pendingDetail: 'Sending clear buffer command to the printer.',
action: _printer.clearBuffer,
successTitle: 'Buffer Cleared',
successDetail: 'Printer buffer was cleared successfully.',
failureTitle: 'Clear Failed',
failureDetail: 'clearBuffer returned false.',
);
}
Future<void> _printTestLabel() async {
if (_busy) {
return;
}
setState(() {
_busy = true;
});
_setFeedback(
_FeedbackTone.loading,
'Printing Test Label',
'Clearing buffer and sending test content.',
);
try {
final bool cleared = await _printer.clearBuffer();
if (!cleared) {
_setFeedback(_FeedbackTone.error, 'Print Failed', 'Clear buffer returned false.');
return;
}
final bool textPrinted =
await _printer.printerfont(40, 30, '3', Rotation.deg0, 1, 1, 'printer_tsc');
final bool barcodePrinted = await _printer.barcode(
40,
90,
BarcodeType.code128,
80,
HumanReadable.visible,
Rotation.deg0,
2,
2,
'1234567890',
);
final bool labelPrinted = await _printer.printlabel(1, 1);
final bool success = textPrinted && barcodePrinted && labelPrinted;
_setFeedback(
success ? _FeedbackTone.success : _FeedbackTone.error,
success ? 'Print Sent' : 'Print Failed',
success
? 'Test label command sequence completed.'
: 'One of the print commands returned false.',
);
} on PlatformException catch (exception) {
_setFeedback(
_FeedbackTone.error,
'Print Failed',
_describePlatformException(exception),
);
} catch (exception) {
_setFeedback(_FeedbackTone.error, 'Print Failed', exception.toString());
} finally {
if (!mounted) {
return;
}
setState(() {
_busy = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Positioned(
top: -120,
right: -40,
child: Container(
width: 240,
height: 240,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: <Color>[Color(0xFFB7E4C7), Color(0x00B7E4C7)],
),
),
),
),
Positioned(
left: -60,
bottom: -80,
child: Container(
width: 220,
height: 220,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: <Color>[Color(0xFFD9ED92), Color(0x00D9ED92)],
),
),
),
),
SafeArea(
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
children: <Widget>[
_HeroHeader(
platformVersion: _platformVersion,
activeConnectionLabel: _activeConnectionLabel,
busy: _busy,
),
const SizedBox(height: 16),
_FeedbackBanner(
tone: _feedbackTone,
title: _feedbackTitle,
detail: _feedbackDetail,
),
const SizedBox(height: 16),
_OverviewGrid(
lastStatus: _lastStatus,
lastBatteryInfo: _lastBatteryInfo,
activeConnectionLabel: _activeConnectionLabel,
),
const SizedBox(height: 16),
SegmentedButton<int>(
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(
value: 0,
label: Text('Android'),
icon: Icon(Icons.android_rounded),
),
ButtonSegment<int>(
value: 1,
label: Text('Windows'),
icon: Icon(Icons.window_rounded),
),
],
selected: <int>{_connectionTabIndex},
onSelectionChanged: (Set<int> value) {
setState(() {
_connectionTabIndex = value.first;
});
},
),
const SizedBox(height: 16),
if (_connectionTabIndex == 0) ...<Widget>[
_ConnectionCard(
title: 'Bluetooth Test',
subtitle: 'Open printer by MAC address.',
icon: Icons.bluetooth_rounded,
accentColor: const Color(0xFF155E75),
child: Column(
children: <Widget>[
TextField(
controller: _bluetoothMacController,
decoration: const InputDecoration(
labelText: 'MAC address',
hintText: '00:19:0E:A0:04:E1',
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _busy ? null : _connectBluetooth,
icon: const Icon(Icons.link_rounded),
label: const Text('Connect Bluetooth'),
),
),
],
),
),
const SizedBox(height: 16),
_ConnectionCard(
title: 'Ethernet Test',
subtitle: 'Open printer by IP and port.',
icon: Icons.lan_rounded,
accentColor: const Color(0xFF0F766E),
child: Column(
children: <Widget>[
TextField(
controller: _ethernetIpController,
decoration: const InputDecoration(
labelText: 'IP address',
hintText: '192.168.1.50',
),
),
const SizedBox(height: 12),
TextField(
controller: _ethernetPortController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Port',
hintText: '9100',
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _busy ? null : _connectEthernet,
icon: const Icon(Icons.cable_rounded),
label: const Text('Connect Ethernet'),
),
),
],
),
),
const SizedBox(height: 16),
_ConnectionCard(
title: 'USB Test',
subtitle: 'Open printer and request permission if needed.',
icon: Icons.usb_rounded,
accentColor: const Color(0xFF1D4ED8),
child: Column(
children: <Widget>[
TextField(
controller: _usbTimeoutController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Timeout ms',
hintText: '3000',
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _busy ? null : _connectUsb,
icon: const Icon(Icons.usb_rounded),
label: const Text('Connect USB'),
),
),
],
),
),
] else ...<Widget>[
_ConnectionCard(
title: 'Windows Port',
subtitle: 'Open printer by port name (e.g. USB001, COM1).',
icon: Icons.print_rounded,
accentColor: const Color(0xFF6B21A8),
child: Column(
children: <Widget>[
TextField(
controller: _windowsPortController,
decoration: const InputDecoration(
labelText: 'Port name',
hintText: 'TSC MA2400',
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _busy ? null : _connectWindowsPort,
icon: const Icon(Icons.link_rounded),
label: const Text('Connect Windows Port'),
),
),
],
),
),
const SizedBox(height: 16),
_ConnectionCard(
title: 'windowsfont Test',
subtitle: 'Print text using a Windows TrueType font (Windows-only).',
icon: Icons.text_fields_rounded,
accentColor: const Color(0xFF7E22CE),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _windowsFontXController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'X'),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _windowsFontYController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Y'),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _windowsFontHeightController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Height (pt)'),
),
),
],
),
const SizedBox(height: 12),
Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _windowsFontRotationController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Rotation',
hintText: '0/90/180/270',
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _windowsFontStyleController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Style',
hintText: '0=normal 1=bold 2=italic',
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _windowsFontUnderlineController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Underline',
hintText: '0=none 1=on',
),
),
),
],
),
const SizedBox(height: 12),
TextField(
controller: _windowsFontFaceController,
decoration: const InputDecoration(
labelText: 'Font face name',
hintText: 'Arial',
),
),
const SizedBox(height: 12),
TextField(
controller: _windowsFontContentController,
decoration: const InputDecoration(
labelText: 'Content',
hintText: 'Hello Windows Font',
),
),
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _busy ? null : _testWindowsFont,
icon: const Icon(Icons.print_rounded),
label: const Text('Print windowsfont'),
),
),
],
),
),
],
const SizedBox(height: 16),
_ConnectionCard(
title: 'Command Console',
subtitle: 'Run printer actions after one connection is open.',
icon: Icons.terminal_rounded,
accentColor: const Color(0xFF7C2D12),
child: Column(
children: <Widget>[
TextField(
controller: _tsplCommandController,
minLines: 5,
maxLines: 9,
decoration: const InputDecoration(
labelText: 'TSPL command',
alignLabelWithHint: true,
),
),
const SizedBox(height: 14),
Wrap(
spacing: 10,
runSpacing: 10,
children: <Widget>[
FilledButton.icon(
onPressed: _busy ? null : _sendRawCommand,
icon: const Icon(Icons.send_rounded),
label: const Text('Send Command'),
),
FilledButton.tonalIcon(
onPressed: _busy ? null : _clearBufferOnly,
icon: const Icon(Icons.cleaning_services_rounded),
label: const Text('Clear Buffer'),
),
FilledButton.tonalIcon(
onPressed: _busy ? null : _printTestLabel,
icon: const Icon(Icons.print_rounded),
label: const Text('Print Test Label'),
),
FilledButton.tonalIcon(
onPressed: _busy ? null : _queryStatus,
icon: const Icon(Icons.info_outline_rounded),
label: const Text('Read Status'),
),
FilledButton.tonalIcon(
onPressed: _busy ? null : _queryBattery,
icon: const Icon(Icons.battery_charging_full_rounded),
label: const Text('Read Battery'),
),
OutlinedButton.icon(
onPressed: _busy ? null : _closePort,
icon: const Icon(Icons.link_off_rounded),
label: const Text('Close Port'),
),
],
),
],
),
),
],
),
),
],
),
);
}
}
class _HeroHeader extends StatelessWidget {
const _HeroHeader({
required this.platformVersion,
required this.activeConnectionLabel,
required this.busy,
});
final String platformVersion;
final String activeConnectionLabel;
final bool busy;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: const LinearGradient(
colors: <Color>[Color(0xFF16302B), Color(0xFF245C4A)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x22000000),
blurRadius: 30,
offset: Offset(0, 12),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Printer Test Console',
style: theme.textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 8),
Text(
'Test Bluetooth, Ethernet, and USB connection flows with direct runtime feedback.',
style: theme.textTheme.bodyLarge?.copyWith(
color: const Color(0xFFD1FAE5),
height: 1.5,
),
),
const SizedBox(height: 16),
Wrap(
spacing: 10,
runSpacing: 10,
children: <Widget>[
_HeaderChip(
label: 'Platform: $platformVersion',
icon: Icons.android_rounded,
),
_HeaderChip(
label: activeConnectionLabel,
icon: Icons.print_rounded,
),
_HeaderChip(
label: busy ? 'Busy' : 'Idle',
icon: busy
? Icons.hourglass_top_rounded
: Icons.check_circle_outline_rounded,
),
],
),
],
),
);
}
}
class _HeaderChip extends StatelessWidget {
const _HeaderChip({required this.label, required this.icon});
final String label;
final IconData icon;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: const Color(0x22FFFFFF),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0x33FFFFFF)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(icon, color: Colors.white, size: 16),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
class _FeedbackBanner extends StatelessWidget {
const _FeedbackBanner({
required this.tone,
required this.title,
required this.detail,
});
final _FeedbackTone tone;
final String title;
final String detail;
@override
Widget build(BuildContext context) {
final Color backgroundColor;
final Color borderColor;
final Color iconColor;
final IconData icon;
switch (tone) {
case _FeedbackTone.success:
backgroundColor = const Color(0xFFEAFBF1);
borderColor = const Color(0xFF86EFAC);
iconColor = const Color(0xFF166534);
icon = Icons.check_circle_rounded;
case _FeedbackTone.error:
backgroundColor = const Color(0xFFFEF3F2);
borderColor = const Color(0xFFFDA29B);
iconColor = const Color(0xFFB42318);
icon = Icons.error_rounded;
case _FeedbackTone.loading:
backgroundColor = const Color(0xFFEEF4FF);
borderColor = const Color(0xFFB2CCFF);
iconColor = const Color(0xFF1D4ED8);
icon = Icons.hourglass_top_rounded;
case _FeedbackTone.idle:
backgroundColor = Colors.white;
borderColor = const Color(0xFFE5E7EB);
iconColor = const Color(0xFF475467);
icon = Icons.notifications_active_outlined;
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: borderColor),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(icon, color: iconColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
color: iconColor,
),
),
const SizedBox(height: 4),
Text(detail),
],
),
),
],
),
);
}
}
class _OverviewGrid extends StatelessWidget {
const _OverviewGrid({
required this.lastStatus,
required this.lastBatteryInfo,
required this.activeConnectionLabel,
});
final String lastStatus;
final String lastBatteryInfo;
final String activeConnectionLabel;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: _MetricCard(
label: 'Connection',
value: activeConnectionLabel,
accentColor: const Color(0xFF0F766E),
icon: Icons.link_rounded,
),
),
const SizedBox(width: 12),
Expanded(
child: _MetricCard(
label: 'Printer Status',
value: lastStatus,
accentColor: const Color(0xFF1D4ED8),
icon: Icons.receipt_long_rounded,
),
),
],
),
const SizedBox(height: 12),
_MetricCard(
label: 'Battery Info',
value: lastBatteryInfo,
accentColor: const Color(0xFF7C2D12),
icon: Icons.battery_charging_full_rounded,
),
],
);
}
}
class _MetricCard extends StatelessWidget {
const _MetricCard({
required this.label,
required this.value,
required this.accentColor,
required this.icon,
});
final String label;
final String value;
final Color accentColor;
final IconData icon;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(22),
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x14000000),
blurRadius: 18,
offset: Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(icon, color: accentColor),
const SizedBox(height: 10),
Text(
label,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: const Color(0xFF667085),
),
),
const SizedBox(height: 6),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _ConnectionCard extends StatelessWidget {
const _ConnectionCard({
required this.title,
required this.subtitle,
required this.icon,
required this.accentColor,
required this.child,
});
final String title;
final String subtitle;
final IconData icon;
final Color accentColor;
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
color: Colors.white,
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x16000000),
blurRadius: 24,
offset: Offset(0, 12),
),
],
),
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: accentColor),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: accentColor,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 4),
Text(subtitle),
],
),
),
],
),
const SizedBox(height: 16),
child,
],
),
),
);
}
}