feat(windows): 向导内置 HiTi Windows 驱动一键安装(对齐 mac 的"装 App 即用"体验)
用户实测定论:CS-2xx 的 USB 是自定义设备(驱动包内含 CS2xxhcd.inf 内核驱动), PavoApi 直打依赖驱动建立的 USB 通道——无驱动时枚举到设备但状态返回离线, "免驱动"假设不成立。产品要求两平台一致:驱动随 App 内置、向导一键安装。 - assets/drivers/CS2xx_Windows_Driver.zip:HiTi 官网 CS-2xx Windows 驱动 (2.6.25.48)整目录 zip(含 CS2xx/Setup64.exe),33MB,不入 git - DriverInstall Windows 分支:Expand-Archive 解压到临时目录 → 递归定位 Setup64.exe(兜底 setup/autorun)→ PowerShell Start-Process -Verb RunAs -Wait 提权安装(UAC 一次)→ 写安装标记(HKCU\Software\zax\desktop-plugin, 系统驱动安装状态无统一可查标志)→ finally 清理临时目录 - 向导第 2 步统一双平台状态机:驱动未装(异步查标记/目录)→"一键安装驱动 (内置安装包)";轮询 3 秒自动刷新安装/连接状态 - bundledInstallerPath 双平台资产路径推导(mac .dmg / win .zip); .gitignore 增 *.zip;assets/drivers/README 更新双平台放置约定
This commit is contained in:
1 parent
527baf7ec5
commit
170e9ddc5e
4 files changed
+156
-53
No files matched your search
@@ -45,3 +45,5 @@ app.*.map.json
|
|||||||
/android/app/release
|
/android/app/release
|
||||||
/dist/
|
/dist/
|
||||||
assets/drivers/*.dmg
|
assets/drivers/*.dmg
|
||||||
|
assets/drivers/*.exe
|
||||||
|
assets/drivers/*.zip
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
# HiTi Mac 驱动安装包目录
|
# HiTi 驱动安装包目录
|
||||||
|
|
||||||
此目录用于打包时**内置 HiTi macOS 打印驱动**(向导"一键安装驱动"功能的数据源):
|
此目录用于打包时**内置 HiTi 打印驱动**(向导"一键安装驱动"功能的数据源):
|
||||||
|
|
||||||
- 构建 macOS 安装包前,把 `HiTiCardTypeMacDrivers_2.4.2.dmg`(约 25MB)放入本目录,
|
- **macOS**:放 `HiTiCardTypeMacDrivers_2.4.2.dmg`(约 25MB)
|
||||||
然后执行 `bash tool/package_macos.sh`——驱动会随 dmg 安装包分发
|
- **Windows**:放 `CS2xx_Windows_Driver.zip`(HiTi 官网 CS-2xx Windows 驱动整个文件夹打成 zip,
|
||||||
- **dmg 文件本身不入 git**(.gitignore 已忽略 `assets/drivers/*.dmg`)
|
内含 `CS2xx/Setup64.exe`;约 33MB)。mac 上打 zip 注意排除 `__MACOSX`:
|
||||||
- 缺少 dmg 时构建不受影响;向导会自动降级为"官网链接手动下载"模式
|
`cd <驱动所在目录> && zip -r CS2xx_Windows_Driver.zip CS2_Driver_x.x.x.x -x "*.DS_Store"`
|
||||||
- **Windows 不需要此目录放任何东西**(PavoApi 直打,驱动可选)
|
- 安装包文件**不入 git**(.gitignore 已忽略 `*.dmg/*.exe/*.zip`),构建安装包的机器放好后再打包
|
||||||
|
- 缺失时构建不受影响;向导自动降级为"官网链接手动下载"模式
|
||||||
+119
-34
@@ -2,44 +2,85 @@ import 'dart:io';
|
|||||||
|
|
||||||
import '../utils/command_runner.dart';
|
import '../utils/command_runner.dart';
|
||||||
|
|
||||||
/// HiTi Mac 驱动(CUPS 过滤器)内置安装:App 资源携带驱动 dmg,
|
/// HiTi 打印驱动内置安装:App 资源携带驱动安装包,向导一键安装。
|
||||||
/// 向导一键挂载静默安装(osascript 提权一次),已装判断 = /Library/Printers/HiTi 存在。
|
///
|
||||||
/// Windows 不适用(PavoApi 直打,官方驱动可选)。
|
/// - macOS:CUPS 过滤器 pkg(dmg 内置),已装判断 = /Library/Printers/HiTi 存在,
|
||||||
|
/// 安装 = 挂载 dmg → osascript 提权 installer → 卸载卷
|
||||||
|
/// - Windows:HiTi 官方驱动 exe(assets/drivers/*.exe 内置),UAC 提权运行;
|
||||||
|
/// 已装判断 = 我们自己的注册表标记(HKCU\Software\zax\desktop-plugin,
|
||||||
|
/// 系统级驱动安装状态无统一可查标志,安装成功后写标记最可靠)
|
||||||
class DriverInstall {
|
class DriverInstall {
|
||||||
static const installedMarker = '/Library/Printers/HiTi';
|
static const installedMarker = '/Library/Printers/HiTi';
|
||||||
|
static const _winMarkKey = r'HKCU\Software\zax\desktop-plugin';
|
||||||
|
static const _winMarkValue = 'HiTiDriverInstalled';
|
||||||
|
|
||||||
static bool get isSupported => Platform.isMacOS;
|
static bool get isSupported => Platform.isMacOS || Platform.isWindows;
|
||||||
|
|
||||||
/// 驱动是否已安装。
|
/// 驱动是否已安装(macOS:驱动目录;Windows:安装标记,同步场景请用 queryInstalled)。
|
||||||
static bool isInstalled() => Directory(installedMarker).existsSync();
|
static bool isInstalled() {
|
||||||
|
if (Platform.isMacOS) return Directory(installedMarker).existsSync();
|
||||||
/// 内置驱动 dmg 的文件系统路径;未打包携带(如开发运行/产物缺失)返回 null。
|
return false; // Windows 走异步 queryInstalled(注册表查询需要子进程)
|
||||||
///
|
|
||||||
/// Flutter assets 在 .app 内的真实路径:
|
|
||||||
/// [app]/Contents/Frameworks/App.framework/Resources/flutter_assets/[asset]
|
|
||||||
/// 从可执行文件位置反推(exe 在 [app]/Contents/MacOS/)。
|
|
||||||
static String? bundledDmgPath() {
|
|
||||||
if (!Platform.isMacOS) return null;
|
|
||||||
final exe = Platform.resolvedExecutable;
|
|
||||||
final exeDir = File(exe).parent.path; // .../Contents/MacOS
|
|
||||||
final driversDir =
|
|
||||||
'$exeDir/../Frameworks/App.framework/Resources/flutter_assets/assets/drivers';
|
|
||||||
final dir = Directory(driversDir);
|
|
||||||
if (!dir.existsSync()) return null;
|
|
||||||
final dmgs = dir
|
|
||||||
.listSync()
|
|
||||||
.whereType<File>()
|
|
||||||
.where((f) => f.path.toLowerCase().endsWith('.dmg'))
|
|
||||||
.toList();
|
|
||||||
return dmgs.isEmpty ? null : dmgs.first.path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从内置 dmg 静默安装驱动(挂载 → 找 pkg → osascript 提权 installer → 卸载卷)。
|
/// Windows:异步查询安装标记。
|
||||||
/// 提权弹窗由系统显示(一次管理员密码),超时 3 分钟。
|
static Future<bool> queryInstalled() async {
|
||||||
static Future<void> installFromDmg(String dmgPath, {void Function(String line)? onLog}) async {
|
if (!Platform.isWindows) return isInstalled();
|
||||||
final log = onLog ?? (_) {};
|
final out = await runCommand(
|
||||||
|
'reg',
|
||||||
|
['query', _winMarkKey, '/v', _winMarkValue],
|
||||||
|
tolerateExit: true,
|
||||||
|
);
|
||||||
|
return out.contains('0x1');
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 只读挂载(挂载点带随机序号,必须从输出解析,不能拼名字)
|
/// 内置驱动安装包的文件系统路径;未打包携带返回 null。
|
||||||
|
///
|
||||||
|
/// Flutter assets 在包内的真实路径(两平台结构不同):
|
||||||
|
/// - macOS:[exe]/../Frameworks/App.framework/Resources/flutter_assets
|
||||||
|
/// - Windows:[exe]/data/flutter_assets
|
||||||
|
static String? bundledInstallerPath() {
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
final exeDir = File(Platform.resolvedExecutable).parent.path;
|
||||||
|
return _firstFile(
|
||||||
|
'$exeDir/../Frameworks/App.framework/Resources/flutter_assets/assets/drivers',
|
||||||
|
'.dmg');
|
||||||
|
}
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
final exeDir = File(Platform.resolvedExecutable).parent.path;
|
||||||
|
return _firstFile('$exeDir/data/flutter_assets/assets/drivers', '.zip');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _firstFile(String dirPath, String ext) {
|
||||||
|
final dir = Directory(dirPath);
|
||||||
|
if (!dir.existsSync()) return null;
|
||||||
|
final files = dir
|
||||||
|
.listSync()
|
||||||
|
.whereType<File>()
|
||||||
|
.where((f) => f.path.toLowerCase().endsWith(ext))
|
||||||
|
.toList();
|
||||||
|
return files.isEmpty ? null : files.first.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 静默安装(macOS)/提权安装(Windows,UAC 确认一次)。
|
||||||
|
static Future<void> install(String installerPath, {void Function(String line)? onLog}) async {
|
||||||
|
final log = onLog ?? (_) {};
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
await _installMac(installerPath, log);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
await _installWindows(installerPath, log);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw UnsupportedError('当前平台不支持驱动安装');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- macOS:dmg → pkg → osascript 提权 ----------------
|
||||||
|
|
||||||
|
static Future<void> _installMac(String dmgPath, void Function(String) log) async {
|
||||||
|
// 挂载点带随机序号,必须从 attach 输出解析,不能拼名字
|
||||||
final attachOut = await runCommand('hdiutil', ['attach', '-nobrowse', '-readonly', dmgPath],
|
final attachOut = await runCommand('hdiutil', ['attach', '-nobrowse', '-readonly', dmgPath],
|
||||||
timeoutMs: 30000);
|
timeoutMs: 30000);
|
||||||
final match = RegExp(r'/Volumes/[^\t\n]+').allMatches(attachOut);
|
final match = RegExp(r'/Volumes/[^\t\n]+').allMatches(attachOut);
|
||||||
@@ -50,7 +91,6 @@ class DriverInstall {
|
|||||||
log('驱动卷已挂载:$mountPoint');
|
log('驱动卷已挂载:$mountPoint');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. 找 pkg
|
|
||||||
final pkg = Directory(mountPoint)
|
final pkg = Directory(mountPoint)
|
||||||
.listSync()
|
.listSync()
|
||||||
.whereType<File>()
|
.whereType<File>()
|
||||||
@@ -61,7 +101,6 @@ class DriverInstall {
|
|||||||
}
|
}
|
||||||
final pkgPath = pkg.first.path;
|
final pkgPath = pkg.first.path;
|
||||||
|
|
||||||
// 3. osascript 提权安装(AppleScript 字符串内嵌套引号用 \" 转义)
|
|
||||||
final script =
|
final script =
|
||||||
'do shell script "installer -pkg \\"$pkgPath\\" -target /" '
|
'do shell script "installer -pkg \\"$pkgPath\\" -target /" '
|
||||||
'with administrator privileges with prompt "安装 HiTi 打印机驱动"';
|
'with administrator privileges with prompt "安装 HiTi 打印机驱动"';
|
||||||
@@ -69,10 +108,56 @@ class DriverInstall {
|
|||||||
await runCommand('osascript', ['-e', script], timeoutMs: 180000);
|
await runCommand('osascript', ['-e', script], timeoutMs: 180000);
|
||||||
log('驱动安装完成');
|
log('驱动安装完成');
|
||||||
} finally {
|
} finally {
|
||||||
// 4. 卸载卷(失败不影响安装结果)
|
|
||||||
try {
|
try {
|
||||||
await runCommand('hdiutil', ['detach', mountPoint, '-force'], tolerateExit: true);
|
await runCommand('hdiutil', ['detach', mountPoint, '-force'], tolerateExit: true);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- Windows:解压 zip → UAC 提权 Setup64 → 写安装标记 ----------------
|
||||||
|
|
||||||
|
static Future<void> _installWindows(String zipPath, void Function(String) log) async {
|
||||||
|
// 1. 解压到临时目录(驱动是"安装器 + 数据文件"的多文件结构,内置为 zip)
|
||||||
|
final extractDir =
|
||||||
|
'${Directory.systemTemp.path}/zax-hiti-driver-${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
log('解压驱动安装包…');
|
||||||
|
await runCommand('powershell', [
|
||||||
|
'-Command',
|
||||||
|
'Expand-Archive -Path "$zipPath" -DestinationPath "$extractDir" -Force'
|
||||||
|
], timeoutMs: 120000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. 递归找安装器(优先 64 位 Setup64.exe,兜底 autorun.exe)
|
||||||
|
final setup = _findFileDeep(extractDir, 'setup64.exe') ??
|
||||||
|
_findFileDeep(extractDir, 'setup.exe') ??
|
||||||
|
_findFileDeep(extractDir, 'autorun.exe');
|
||||||
|
if (setup == null) {
|
||||||
|
throw StateError('驱动包内未找到安装程序(Setup64.exe)');
|
||||||
|
}
|
||||||
|
log('开始安装 HiTi 打印机驱动(请在 UAC 弹窗中确认,安装向导可能需要点几次"下一步")…');
|
||||||
|
// -Verb RunAs 触发 UAC;-Wait 等安装程序退出(用户取消 UAC 时 PowerShell 报错)
|
||||||
|
final script = 'Start-Process -FilePath "$setup" -Verb RunAs -Wait';
|
||||||
|
await runCommand('powershell', ['-Command', script], timeoutMs: 600000);
|
||||||
|
log('驱动安装程序已退出');
|
||||||
|
} finally {
|
||||||
|
// 3. 清理临时目录(失败不影响安装结果)
|
||||||
|
try {
|
||||||
|
await Directory(extractDir).delete(recursive: true);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 写安装标记(我们自己的注册表键;0x1 = 已装)
|
||||||
|
await runCommand('reg',
|
||||||
|
['add', _winMarkKey, '/v', _winMarkValue, '/t', 'REG_DWORD', '/d', '1', '/f']);
|
||||||
|
log('已记录驱动安装状态');
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _findFileDeep(String root, String lowerName) {
|
||||||
|
final hits = Directory(root)
|
||||||
|
.listSync(recursive: true, followLinks: false)
|
||||||
|
.whereType<File>()
|
||||||
|
.where((f) => f.uri.pathSegments.last.toLowerCase() == lowerName)
|
||||||
|
.toList();
|
||||||
|
return hits.isEmpty ? null : hits.first.path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+27
-12
@@ -48,17 +48,22 @@ class _SetupWizardState extends State<SetupWizard> {
|
|||||||
String? _driverInstallError;
|
String? _driverInstallError;
|
||||||
PrinterStatus? _printerStatus;
|
PrinterStatus? _printerStatus;
|
||||||
|
|
||||||
|
/// 驱动是否已装(null=查询中;mac=驱动目录,win=注册表安装标记)
|
||||||
|
bool? _driverInstalled;
|
||||||
|
|
||||||
Agent get _agent => widget.agent;
|
Agent get _agent => widget.agent;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_checkPrinter();
|
_checkPrinter();
|
||||||
|
_queryDriver();
|
||||||
// 第 2 步期间自动轮询:用户手动装驱动 / 插上打印机后页面自动刷新
|
// 第 2 步期间自动轮询:用户手动装驱动 / 插上打印机后页面自动刷新
|
||||||
// (驱动目录判断零成本;getStatus 走 lpoptions,3s 一次可接受)
|
// (驱动目录判断零成本;getStatus 走 lpoptions,3s 一次可接受)
|
||||||
_pollTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
_pollTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||||
if (_step == 1 && mounted && !_driverInstalling) {
|
if (_step == 1 && mounted && !_driverInstalling) {
|
||||||
_checkPrinter(silent: true);
|
_checkPrinter(silent: true);
|
||||||
|
_queryDriver();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -71,6 +76,11 @@ class _SetupWizardState extends State<SetupWizard> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _queryDriver() async {
|
||||||
|
final installed = await DriverInstall.queryInstalled();
|
||||||
|
if (mounted) setState(() => _driverInstalled = installed);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _checkPrinter({bool silent = false}) async {
|
Future<void> _checkPrinter({bool silent = false}) async {
|
||||||
if (!silent) setState(() => _printerChecking = true);
|
if (!silent) setState(() => _printerChecking = true);
|
||||||
try {
|
try {
|
||||||
@@ -192,16 +202,17 @@ class _SetupWizardState extends State<SetupWizard> {
|
|||||||
|
|
||||||
// ---------------- Step 1:打印机检测 ----------------
|
// ---------------- Step 1:打印机检测 ----------------
|
||||||
|
|
||||||
/// 一键安装内置驱动(mac):装完自动重检(队列要等打印机连上/系统识别后才出现)。
|
/// 一键安装内置驱动:装完自动重检(队列要等打印机连上/系统识别后才出现)。
|
||||||
Future<void> _installDriver() async {
|
Future<void> _installDriver() async {
|
||||||
final dmg = DriverInstall.bundledDmgPath();
|
final installer = DriverInstall.bundledInstallerPath();
|
||||||
if (dmg == null) return;
|
if (installer == null) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_driverInstalling = true;
|
_driverInstalling = true;
|
||||||
_driverInstallError = null;
|
_driverInstallError = null;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await DriverInstall.installFromDmg(dmg, onLog: _agent.log.write);
|
await DriverInstall.install(installer, onLog: _agent.log.write);
|
||||||
|
await _queryDriver();
|
||||||
await _checkPrinter();
|
await _checkPrinter();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) setState(() => _driverInstallError = '$e');
|
if (mounted) setState(() => _driverInstallError = '$e');
|
||||||
@@ -213,8 +224,8 @@ class _SetupWizardState extends State<SetupWizard> {
|
|||||||
Widget _stepPrinter(ColorScheme scheme) {
|
Widget _stepPrinter(ColorScheme scheme) {
|
||||||
final s = _printerStatus;
|
final s = _printerStatus;
|
||||||
final ready = s != null && s.online;
|
final ready = s != null && s.online;
|
||||||
final driverMissing = Platform.isMacOS && !DriverInstall.isInstalled();
|
final driverMissing = _driverInstalled == false;
|
||||||
final bundledDmg = Platform.isMacOS ? DriverInstall.bundledDmgPath() : null;
|
final bundledInstaller = DriverInstall.bundledInstallerPath();
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -227,17 +238,19 @@ class _SetupWizardState extends State<SetupWizard> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// mac:驱动未装 → 首选一键安装内置驱动,其次官网链接
|
// 驱动未装 → 首选一键安装内置驱动,其次官网链接(mac/win 通用)
|
||||||
if (driverMissing) ...[
|
if (driverMissing) ...[
|
||||||
Text(
|
Text(
|
||||||
'本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。',
|
Platform.isMacOS
|
||||||
|
? '本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。'
|
||||||
|
: '本机尚未安装 HiTi 打印机驱动。直打通道需要驱动建立的 USB 连接,装好即可自动识别打印机。',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6),
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
onPressed: _driverInstalling || bundledDmg == null ? null : _installDriver,
|
onPressed: _driverInstalling || bundledInstaller == null ? null : _installDriver,
|
||||||
icon: _driverInstalling
|
icon: _driverInstalling
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 14,
|
width: 14,
|
||||||
@@ -246,7 +259,7 @@ class _SetupWizardState extends State<SetupWizard> {
|
|||||||
)
|
)
|
||||||
: const Icon(Icons.download, size: 16),
|
: const Icon(Icons.download, size: 16),
|
||||||
label: Text(_driverInstalling
|
label: Text(_driverInstalling
|
||||||
? '正在安装…(弹窗中输入管理员密码)'
|
? '正在安装…(请在系统弹窗中确认)'
|
||||||
: '一键安装驱动(内置安装包)'),
|
: '一键安装驱动(内置安装包)'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -257,7 +270,7 @@ class _SetupWizardState extends State<SetupWizard> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (bundledDmg == null)
|
if (bundledInstaller == null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -278,7 +291,9 @@ class _SetupWizardState extends State<SetupWizard> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'安装完成后页面会自动刷新(也可手动重新检测);随后 USB 连接打印机并开机,系统识别出现打印队列即完成。',
|
Platform.isMacOS
|
||||||
|
? '安装完成后:USB 连接打印机并开机,系统识别出现打印队列即完成。'
|
||||||
|
: '安装完成后 USB 连接打印机并开机,回到这里重新检测即完成。',
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|||||||
Reference in new issue
Block a user