diff --git a/Kiosk/lib/db/database_helper.dart b/Kiosk/lib/db/database_helper.dart index 97e653a..ebd35b6 100644 --- a/Kiosk/lib/db/database_helper.dart +++ b/Kiosk/lib/db/database_helper.dart @@ -124,6 +124,17 @@ class DatabaseHelper { return maps.map((json) => Product.fromJson(json)).toList(); } + Future> searchProducts(String query) async { + final db = await instance.database; + final maps = await db.query( + 'products', + where: 'barcode LIKE ? OR name LIKE ?', + whereArgs: ['$query%', '%$query%'], + limit: 20, + ); + return maps.map((json) => Product.fromJson(json)).toList(); + } + Future clearProducts() async { final db = await instance.database; await db.delete('products'); diff --git a/Kiosk/lib/l10n/app_en.arb b/Kiosk/lib/l10n/app_en.arb index bcfe823..2d07b12 100644 --- a/Kiosk/lib/l10n/app_en.arb +++ b/Kiosk/lib/l10n/app_en.arb @@ -64,6 +64,12 @@ "searchApps": "Search apps", "noAppsFound": "No apps found", "homeAppOpenFailed": "Can't open {package}. Opening Launcher instead.", + "noBarcodeProductsTitle": "No Barcode Products", + "noBarcodeProductsEmpty": "No no-barcode products", + "categories": "Categories", + "categoryAll": "All", + "categoryUncategorized": "Uncategorized", + "searchProducts": "Search products", "clear": "Clear", "save": "Save" } diff --git a/Kiosk/lib/l10n/app_localizations.dart b/Kiosk/lib/l10n/app_localizations.dart index 495dfed..68031a9 100644 --- a/Kiosk/lib/l10n/app_localizations.dart +++ b/Kiosk/lib/l10n/app_localizations.dart @@ -488,6 +488,42 @@ abstract class AppLocalizations { /// **'Can\'t open {package}. Opening Launcher instead.'** String homeAppOpenFailed(Object package); + /// No description provided for @noBarcodeProductsTitle. + /// + /// In en, this message translates to: + /// **'No Barcode Products'** + String get noBarcodeProductsTitle; + + /// No description provided for @noBarcodeProductsEmpty. + /// + /// In en, this message translates to: + /// **'No no-barcode products'** + String get noBarcodeProductsEmpty; + + /// No description provided for @categories. + /// + /// In en, this message translates to: + /// **'Categories'** + String get categories; + + /// No description provided for @categoryAll. + /// + /// In en, this message translates to: + /// **'All'** + String get categoryAll; + + /// No description provided for @categoryUncategorized. + /// + /// In en, this message translates to: + /// **'Uncategorized'** + String get categoryUncategorized; + + /// No description provided for @searchProducts. + /// + /// In en, this message translates to: + /// **'Search products'** + String get searchProducts; + /// No description provided for @clear. /// /// In en, this message translates to: diff --git a/Kiosk/lib/l10n/app_localizations_en.dart b/Kiosk/lib/l10n/app_localizations_en.dart index 58823a7..77993b6 100644 --- a/Kiosk/lib/l10n/app_localizations_en.dart +++ b/Kiosk/lib/l10n/app_localizations_en.dart @@ -229,6 +229,24 @@ class AppLocalizationsEn extends AppLocalizations { return 'Can\'t open $package. Opening Launcher instead.'; } + @override + String get noBarcodeProductsTitle => 'No Barcode Products'; + + @override + String get noBarcodeProductsEmpty => 'No no-barcode products'; + + @override + String get categories => 'Categories'; + + @override + String get categoryAll => 'All'; + + @override + String get categoryUncategorized => 'Uncategorized'; + + @override + String get searchProducts => 'Search products'; + @override String get clear => 'Clear'; diff --git a/Kiosk/lib/l10n/app_localizations_zh.dart b/Kiosk/lib/l10n/app_localizations_zh.dart index fc6e174..6bce3fb 100644 --- a/Kiosk/lib/l10n/app_localizations_zh.dart +++ b/Kiosk/lib/l10n/app_localizations_zh.dart @@ -225,6 +225,24 @@ class AppLocalizationsZh extends AppLocalizations { return '无法打开 $package,将打开桌面启动器。'; } + @override + String get noBarcodeProductsTitle => '无条码商品'; + + @override + String get noBarcodeProductsEmpty => '暂无无条码商品'; + + @override + String get categories => '分类'; + + @override + String get categoryAll => '全部'; + + @override + String get categoryUncategorized => '未分类'; + + @override + String get searchProducts => '搜索商品'; + @override String get clear => '清除'; diff --git a/Kiosk/lib/l10n/app_zh.arb b/Kiosk/lib/l10n/app_zh.arb index 8222a95..53167a8 100644 --- a/Kiosk/lib/l10n/app_zh.arb +++ b/Kiosk/lib/l10n/app_zh.arb @@ -64,6 +64,12 @@ "searchApps": "搜索应用", "noAppsFound": "未找到应用", "homeAppOpenFailed": "无法打开 {package},将打开桌面启动器。", + "noBarcodeProductsTitle": "无条码商品", + "noBarcodeProductsEmpty": "暂无无条码商品", + "categories": "分类", + "categoryAll": "全部", + "categoryUncategorized": "未分类", + "searchProducts": "搜索商品", "clear": "清除", "save": "保存" } diff --git a/Kiosk/lib/screens/main_screen.dart b/Kiosk/lib/screens/main_screen.dart index 2897db2..01c5adc 100644 --- a/Kiosk/lib/screens/main_screen.dart +++ b/Kiosk/lib/screens/main_screen.dart @@ -8,6 +8,8 @@ import 'package:kiosk/db/database_helper.dart'; import 'package:kiosk/models/product.dart'; import 'package:kiosk/models/order.dart' as model; // Alias to avoid conflict if needed import 'package:kiosk/screens/payment_screen.dart'; +import 'package:kiosk/screens/manual_barcode_dialog.dart'; +import 'package:kiosk/screens/no_barcode_products_dialog.dart'; import 'package:kiosk/screens/settings_screen.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:kiosk/l10n/app_localizations.dart'; @@ -260,6 +262,24 @@ class _MainScreenState extends State { ); } + Future _openNoBarcodePicker() async { + final l10n = AppLocalizations.of(context)!; + final messenger = ScaffoldMessenger.of(context); + await showDialog( + context: context, + builder: (context) { + return NoBarcodeProductsDialog( + onAdd: (product) { + _addToCart(product); + messenger.showSnackBar( + SnackBar(content: Text(l10n.addedProduct(product.name))), + ); + }, + ); + }, + ); + } + Future _handleBarcodeDetect(BarcodeCapture capture) async { // Only process logic if not already processing if (_isProcessing) return; @@ -649,19 +669,21 @@ class _MainScreenState extends State { Row( children: [ TextButton.icon( - onPressed: () { - // Manual barcode entry dialog - // Placeholder + onPressed: () async { + final product = await showDialog( + context: context, + builder: (context) => const ManualBarcodeDialog(), + ); + if (product != null) { + _addToCart(product); + } }, icon: const Icon(Icons.keyboard, color: Colors.grey), label: Text(l10n.inputBarcode, style: const TextStyle(color: Colors.grey)), ), const SizedBox(width: 16), TextButton.icon( - onPressed: () { - // No barcode selection - // Placeholder - }, + onPressed: _openNoBarcodePicker, icon: const Icon(Icons.grid_view, color: Colors.grey), label: Text(l10n.noBarcodeItem, style: const TextStyle(color: Colors.grey)), ), diff --git a/Kiosk/lib/screens/manual_barcode_dialog.dart b/Kiosk/lib/screens/manual_barcode_dialog.dart new file mode 100644 index 0000000..24f660e --- /dev/null +++ b/Kiosk/lib/screens/manual_barcode_dialog.dart @@ -0,0 +1,230 @@ +import 'package:flutter/material.dart'; +import 'package:kiosk/db/database_helper.dart'; +import 'package:kiosk/models/product.dart'; +import 'package:intl/intl.dart'; +import 'package:kiosk/l10n/app_localizations.dart'; + +class ManualBarcodeDialog extends StatefulWidget { + const ManualBarcodeDialog({super.key}); + + @override + State createState() => _ManualBarcodeDialogState(); +} + +class _ManualBarcodeDialogState extends State { + String _input = ''; + List _suggestions = []; + bool _isLoading = false; + final _db = DatabaseHelper.instance; + + void _onKeyTap(String key) { + setState(() { + _input += key; + }); + _search(); + } + + void _onBackspace() { + if (_input.isNotEmpty) { + setState(() { + _input = _input.substring(0, _input.length - 1); + }); + _search(); + } + } + + void _onClear() { + setState(() { + _input = ''; + _suggestions = []; + }); + } + + Future _search() async { + if (_input.isEmpty) { + setState(() { + _suggestions = []; + }); + return; + } + + setState(() => _isLoading = true); + try { + final results = await _db.searchProducts(_input); + if (mounted) { + setState(() { + _suggestions = results; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) setState(() => _isLoading = false); + } + } + + void _onProductSelected(Product product) { + Navigator.pop(context, product); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final currencyFormat = NumberFormat.currency(symbol: '¥'); + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Container( + width: 600, + height: 700, + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(l10n.inputBarcode, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)), + ], + ), + const Divider(), + + // Input Display + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _input.isEmpty ? l10n.inputBarcode : _input, + style: TextStyle( + fontSize: 32, + color: _input.isEmpty ? Colors.grey : Colors.black, + letterSpacing: 2, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + + // Suggestions List + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _suggestions.isEmpty + ? Center(child: Text(_input.isEmpty ? '' : l10n.productNotFound(_input))) + : ListView.builder( + itemCount: _suggestions.length, + itemBuilder: (context, index) { + final product = _suggestions[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + leading: const Icon(Icons.shopping_bag_outlined), + title: Text(product.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + subtitle: Text('${product.barcode} ${product.brand ?? ''}'), + trailing: Text( + currencyFormat.format(product.price), + style: const TextStyle(fontSize: 18, color: Colors.red, fontWeight: FontWeight.bold), + ), + onTap: () => _onProductSelected(product), + ), + ); + }, + ), + ), + const SizedBox(height: 16), + + // Numeric Keypad + SizedBox( + height: 300, + child: Row( + children: [ + Expanded( + flex: 3, + child: GridView.count( + crossAxisCount: 3, + childAspectRatio: 1.5, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + physics: const NeverScrollableScrollPhysics(), + children: [ + for (var i = 1; i <= 9; i++) + _buildKey(i.toString()), + _buildKey('0'), + // Using a distinct visual style for special keys if needed + _buildKey('.', onTap: () => _onKeyTap('.')), + _buildBackspace(), + ], + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 1, + child: Column( + children: [ + Expanded(child: _buildActionButton(l10n.clear, Colors.orange, _onClear)), + const SizedBox(height: 10), + Expanded(child: _buildActionButton(l10n.close, Colors.grey, () => Navigator.pop(context))), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildKey(String label, {VoidCallback? onTap}) { + return InkWell( + onTap: onTap ?? () => _onKeyTap(label), + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(1, 1)), + ], + ), + alignment: Alignment.center, + child: Text(label, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), + ), + ); + } + + Widget _buildBackspace() { + return InkWell( + onTap: _onBackspace, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: const Icon(Icons.backspace, size: 28), + ), + ); + } + + Widget _buildActionButton(String label, Color color, VoidCallback onTap) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text(label, style: const TextStyle(fontSize: 20, color: Colors.white, fontWeight: FontWeight.bold)), + ), + ); + } +} diff --git a/Kiosk/lib/screens/no_barcode_products_dialog.dart b/Kiosk/lib/screens/no_barcode_products_dialog.dart new file mode 100644 index 0000000..c5e1e5a --- /dev/null +++ b/Kiosk/lib/screens/no_barcode_products_dialog.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:kiosk/db/database_helper.dart'; +import 'package:kiosk/models/product.dart'; +import 'package:kiosk/l10n/app_localizations.dart'; + +class NoBarcodeProductsDialog extends StatefulWidget { + final ValueChanged onAdd; + + const NoBarcodeProductsDialog({super.key, required this.onAdd}); + + @override + State createState() => _NoBarcodeProductsDialogState(); +} + +class _NoBarcodeProductsDialogState extends State { + late final Future> _futureProducts; + String _selectedCategory = ''; + String _query = ''; + + @override + void initState() { + super.initState(); + _futureProducts = DatabaseHelper.instance.getAllProducts(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Dialog( + insetPadding: const EdgeInsets.all(16), + child: SizedBox( + width: 980, + height: 640, + child: FutureBuilder>( + future: _futureProducts, + builder: (context, snapshot) { + final all = snapshot.data ?? const []; + final noBarcode = + all.where((p) => p.barcode.startsWith('NB-')).toList(growable: false); + final categories = [ + l10n.categoryAll, + ...{ + for (final p in noBarcode) + (p.type == null || p.type!.trim().isEmpty) + ? l10n.categoryUncategorized + : p.type!.trim(), + } + ]..sort((a, b) { + if (a == l10n.categoryAll) return -1; + if (b == l10n.categoryAll) return 1; + if (a == l10n.categoryUncategorized) return 1; + if (b == l10n.categoryUncategorized) return -1; + return a.toLowerCase().compareTo(b.toLowerCase()); + }); + + final effectiveCategory = + _selectedCategory.isEmpty ? l10n.categoryAll : _selectedCategory; + final filtered = noBarcode.where((p) { + final name = p.name.toLowerCase(); + final q = _query.trim().toLowerCase(); + if (q.isNotEmpty && !name.contains(q)) return false; + if (effectiveCategory == l10n.categoryAll) return true; + final type = p.type?.trim(); + if (effectiveCategory == l10n.categoryUncategorized) { + return type == null || type.isEmpty; + } + return type == effectiveCategory; + }).toList() + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Expanded( + child: Text( + l10n.noBarcodeProductsTitle, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close), + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: Row( + children: [ + SizedBox( + width: 200, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + l10n.categories, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: categories.length, + itemBuilder: (context, index) { + final c = categories[index]; + final selected = c == effectiveCategory; + return ListTile( + dense: true, + selected: selected, + title: Text(c), + onTap: () => setState(() => _selectedCategory = c), + ); + }, + ), + ), + ], + ), + ), + const VerticalDivider(width: 1), + Expanded( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + TextField( + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + hintText: l10n.searchProducts, + border: const OutlineInputBorder(), + isDense: true, + ), + onChanged: (v) => setState(() => _query = v), + ), + const SizedBox(height: 12), + Expanded( + child: snapshot.connectionState == ConnectionState.waiting + ? const Center(child: CircularProgressIndicator()) + : filtered.isEmpty + ? Center(child: Text(l10n.noBarcodeProductsEmpty)) + : GridView.builder( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 220, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 2.6, + ), + itemCount: filtered.length, + itemBuilder: (context, index) { + final p = filtered[index]; + return ElevatedButton( + onPressed: () => widget.onAdd(p), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + p.name, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/Manager/lib/db/database_helper.dart b/Manager/lib/db/database_helper.dart index c4f4721..6b5499f 100644 --- a/Manager/lib/db/database_helper.dart +++ b/Manager/lib/db/database_helper.dart @@ -120,6 +120,25 @@ class DatabaseHelper { ); } + Future getNextNoBarcodeId() async { + final db = await instance.database; + final rows = await db.query( + 'products', + columns: ['barcode'], + where: 'barcode LIKE ?', + whereArgs: const ['NB-%'], + ); + var maxN = 0; + for (final row in rows) { + final raw = row['barcode']?.toString() ?? ''; + if (!raw.startsWith('NB-')) continue; + final n = int.tryParse(raw.substring(3)); + if (n == null) continue; + if (n > maxN) maxN = n; + } + return 'NB-${maxN + 1}'; + } + // Kiosk Methods Future insertKiosk(Kiosk kiosk) async { final db = await instance.database; diff --git a/Manager/lib/l10n/app_en.arb b/Manager/lib/l10n/app_en.arb index 2a742b3..fcc5409 100644 --- a/Manager/lib/l10n/app_en.arb +++ b/Manager/lib/l10n/app_en.arb @@ -20,6 +20,7 @@ "productName": "Product Name", "price": "Price", "barcodeLabel": "Barcode", + "noBarcodeProduct": "No barcode product", "saveProduct": "Save Product", "save": "Save", "successSave": "Product saved successfully", diff --git a/Manager/lib/l10n/app_localizations.dart b/Manager/lib/l10n/app_localizations.dart index 25624d0..a52e022 100644 --- a/Manager/lib/l10n/app_localizations.dart +++ b/Manager/lib/l10n/app_localizations.dart @@ -224,6 +224,12 @@ abstract class AppLocalizations { /// **'Barcode'** String get barcodeLabel; + /// No description provided for @noBarcodeProduct. + /// + /// In en, this message translates to: + /// **'No barcode product'** + String get noBarcodeProduct; + /// No description provided for @saveProduct. /// /// In en, this message translates to: diff --git a/Manager/lib/l10n/app_localizations_en.dart b/Manager/lib/l10n/app_localizations_en.dart index 6e9ede8..8034c35 100644 --- a/Manager/lib/l10n/app_localizations_en.dart +++ b/Manager/lib/l10n/app_localizations_en.dart @@ -77,6 +77,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get barcodeLabel => 'Barcode'; + @override + String get noBarcodeProduct => 'No barcode product'; + @override String get saveProduct => 'Save Product'; diff --git a/Manager/lib/l10n/app_localizations_zh.dart b/Manager/lib/l10n/app_localizations_zh.dart index 83f3a28..3b265be 100644 --- a/Manager/lib/l10n/app_localizations_zh.dart +++ b/Manager/lib/l10n/app_localizations_zh.dart @@ -77,6 +77,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get barcodeLabel => '条形码'; + @override + String get noBarcodeProduct => '无条码商品'; + @override String get saveProduct => '保存商品'; diff --git a/Manager/lib/l10n/app_zh.arb b/Manager/lib/l10n/app_zh.arb index 07ba5a9..08507f0 100644 --- a/Manager/lib/l10n/app_zh.arb +++ b/Manager/lib/l10n/app_zh.arb @@ -20,6 +20,7 @@ "productName": "商品名称", "price": "价格", "barcodeLabel": "条形码", + "noBarcodeProduct": "无条码商品", "saveProduct": "保存商品", "save": "保存", "successSave": "商品保存成功", diff --git a/Manager/lib/screens/product_form_screen.dart b/Manager/lib/screens/product_form_screen.dart index 0e32256..f026d47 100644 --- a/Manager/lib/screens/product_form_screen.dart +++ b/Manager/lib/screens/product_form_screen.dart @@ -27,6 +27,8 @@ class _ProductFormScreenState extends State { final KioskConnectionService _connectionService = KioskConnectionService(); bool _isLoading = false; bool _isSaving = false; + bool _noBarcodeMode = false; + bool _isEditingExistingNoBarcode = false; @override void initState() { @@ -39,6 +41,8 @@ class _ProductFormScreenState extends State { }); if (widget.initialBarcode != null) { _barcodeController.text = widget.initialBarcode!; + _isEditingExistingNoBarcode = widget.initialBarcode!.startsWith('NB-'); + _noBarcodeMode = _isEditingExistingNoBarcode; _loadProduct(widget.initialBarcode!); } } @@ -169,8 +173,13 @@ class _ProductFormScreenState extends State { _normalizePriceOnBlur(); setState(() => _isSaving = true); + final barcode = _noBarcodeMode + ? (_isEditingExistingNoBarcode + ? _barcodeController.text + : await DatabaseHelper.instance.getNextNoBarcodeId()) + : _barcodeController.text.trim(); final product = Product( - barcode: _barcodeController.text, + barcode: barcode, name: _nameController.text, price: double.parse(_priceController.text.trim()), lastUpdated: DateTime.now().millisecondsSinceEpoch, @@ -224,6 +233,7 @@ class _ProductFormScreenState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final isConnected = _connectionService.hasConnectedKiosk; + final canToggleNoBarcode = widget.initialBarcode == null; return Scaffold( appBar: AppBar(title: Text(l10n.addProduct)), body: _isLoading @@ -243,25 +253,39 @@ class _ProductFormScreenState extends State { textAlign: TextAlign.center, ), ), - Row( - children: [ - Expanded( - child: TextFormField( - controller: _barcodeController, - decoration: InputDecoration(labelText: l10n.barcodeLabel), - enabled: isConnected && !_isSaving, - validator: (value) => - value!.isEmpty ? l10n.barcodeRequired : null, + if (canToggleNoBarcode) + SwitchListTile( + title: Text(l10n.noBarcodeProduct), + value: _noBarcodeMode, + onChanged: isConnected && !_isSaving + ? (v) { + setState(() { + _noBarcodeMode = v; + if (v) { + _barcodeController.clear(); + } + }); + } + : null, + ), + if (!_noBarcodeMode) + Row( + children: [ + Expanded( + child: TextFormField( + controller: _barcodeController, + decoration: InputDecoration(labelText: l10n.barcodeLabel), + enabled: isConnected && !_isSaving, + validator: (value) => + value!.isEmpty ? l10n.barcodeRequired : null, + ), ), - ), - IconButton( - icon: const Icon(Icons.qr_code_scanner), - onPressed: isConnected && !_isSaving - ? _scanBarcode - : null, - ), - ], - ), + IconButton( + icon: const Icon(Icons.qr_code_scanner), + onPressed: isConnected && !_isSaving ? _scanBarcode : null, + ), + ], + ), TextFormField( controller: _nameController, decoration: InputDecoration(labelText: l10n.productName),