flutter_barcode_scanner_sdk 0.1.1 copy "flutter_barcode_scanner_sdk: ^0.1.1" to clipboard
flutter_barcode_scanner_sdk: ^0.1.1 copied to clipboard

High-throughput barcode and QR scanner SDK for Flutter apps.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_barcode_scanner_sdk/flutter_barcode_scanner_sdk.dart';

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

enum ShowcaseLanguage { english, hebrew }

enum FormatPreset { all, qrAndCode128, twoDimensionalOnly, oneDimensionalOnly }

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0A1C58)),
        useMaterial3: true,
      ),
      home: const ScannerShowcaseScreen(),
    );
  }
}

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

  @override
  State<ScannerShowcaseScreen> createState() => _ScannerShowcaseScreenState();
}

class _ScannerShowcaseScreenState extends State<ScannerShowcaseScreen> {
  static const _appBarColors = <Color>[
    Color(0xFF0A1C58),
    Color(0xFF0D3B2E),
    Color(0xFF4A1D1F),
    Color(0xFF263238),
  ];

  static const _overlayOptions = <double>[0.30, 0.45, 0.60, 0.72];
  static const _pausedBorderColors = <Color>[
    Color(0xFFE53935),
    Color(0xFFFFB300),
    Color(0xFF00C853),
    Color(0xFF40C4FF),
  ];

  ShowcaseLanguage _language = ShowcaseLanguage.english;
  bool _isContinuousLoop = false;
  bool _isLaunchingScanner = false;
  bool _showFlashButton = true;
  bool _showCameraSwitchButton = true;
  bool _initialTorchEnabled = false;
  bool _statusBarTransparent = false;
  bool _appBarTransparent = false;
  bool _scanWindowEnabled = true;
  bool _showEmbeddedScanner = false;
  bool _embeddedAutoRequestCameraPermission = true;
  bool _embeddedAutoStart = true;
  bool _embeddedAutoPauseOnScan = true;
  bool _embeddedFreezePreviewWhenPaused = false;
  bool _embeddedShowPauseResumeButton = false;
  BarcodeCameraLens _initialCameraLens = BarcodeCameraLens.back;
  FlutterBarcodeScannerStatusBarIconBrightness _statusBarIconBrightness =
      FlutterBarcodeScannerStatusBarIconBrightness.light;
  TextDirection? _scannerTextDirection = TextDirection.ltr;
  FormatPreset _formatPreset = FormatPreset.qrAndCode128;
  double _scanWindowWidthFactor = 0.58;
  double _scanWindowHeightFactor = 0.58;
  double _scanWindowCornerRadius = 18;
  int _appBarColorIndex = 0;
  int _overlayOpacityIndex = 2;
  int _pausedBorderColorIndex = 0;

  FlutterBarcodeScanResult? _lastResult;
  final FlutterBarcodeScannerController _embeddedController =
      FlutterBarcodeScannerController();
  final List<FlutterBarcodeScanResult> _scanHistory =
      <FlutterBarcodeScanResult>[];

  @override
  void dispose() {
    _embeddedController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final effectiveDirection =
        _scannerTextDirection ?? Directionality.of(context);
    final isRtl = effectiveDirection == TextDirection.rtl;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Scanner SDK Showcase'),
        actions: [
          FilledButton.tonalIcon(
            onPressed: _isLaunchingScanner ? null : _startScannerFlow,
            icon: const Icon(Icons.qr_code_scanner),
            label: Text(_isLaunchingScanner ? 'Scanning…' : 'Start'),
          ),
          const SizedBox(width: 12),
        ],
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final isCompact = constraints.maxWidth < 1100;
          final controlsSections = _buildControlsSections(context, isRtl);
          final resultsSections = _buildResultsSections(context, scheme, isRtl);

          final controlsPane = ListView(
            padding: const EdgeInsets.all(16),
            children: controlsSections,
          );

          final resultsPane = Container(
            color: scheme.surfaceContainerLowest,
            padding: const EdgeInsets.all(16),
            child: ListView(children: resultsSections),
          );

          if (isCompact) {
            return ListView(
              padding: const EdgeInsets.all(16),
              children: [
                ...controlsSections,
                const SizedBox(height: 8),
                Container(
                  color: scheme.surfaceContainerLowest,
                  padding: const EdgeInsets.all(16),
                  child: Column(children: resultsSections),
                ),
              ],
            );
          }

          return Row(
            children: [
              Expanded(flex: 8, child: controlsPane),
              Expanded(flex: 5, child: resultsPane),
            ],
          );
        },
      ),
    );
  }

  List<Widget> _buildControlsSections(BuildContext context, bool isRtl) {
    return [
      _SectionCard(
        title: 'Scan Flow',
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _isContinuousLoop,
              title: const Text('Continuous loop'),
              subtitle: const Text(
                'Reopen the native scanner after each successful result until the user cancels.',
              ),
              onChanged: (value) {
                setState(() {
                  _isContinuousLoop = value;
                });
              },
            ),
          ],
        ),
      ),
      const SizedBox(height: 16),
      _SectionCard(
        title: 'Embedded Scanner',
        child: Column(
          children: [
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _showEmbeddedScanner,
              title: const Text('Show embedded scanner widget'),
              subtitle: const Text(
                'Uses the same native engines inside a Flutter layout.',
              ),
              onChanged: (value) {
                setState(() {
                  _showEmbeddedScanner = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _embeddedAutoRequestCameraPermission,
              title: const Text('Auto-request camera permission'),
              onChanged: (value) {
                setState(() {
                  _embeddedAutoRequestCameraPermission = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _embeddedAutoStart,
              title: const Text('Embedded auto-start'),
              onChanged: (value) {
                setState(() {
                  _embeddedAutoStart = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _embeddedAutoPauseOnScan,
              title: const Text('Embedded auto-pause after result'),
              onChanged: (value) {
                setState(() {
                  _embeddedAutoPauseOnScan = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _embeddedFreezePreviewWhenPaused,
              title: const Text('Freeze preview while paused'),
              subtitle: const Text(
                'Auto-pause freezes on the detected barcode frame.',
              ),
              onChanged: (value) {
                setState(() {
                  _embeddedFreezePreviewWhenPaused = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _embeddedShowPauseResumeButton,
              title: const Text('Show pause / resume overlay button'),
              onChanged: (value) {
                setState(() {
                  _embeddedShowPauseResumeButton = value;
                });
              },
            ),
            const SizedBox(height: 12),
            _ColorChoices(
              label: 'Paused scan-window border color',
              colors: _pausedBorderColors,
              selectedIndex: _pausedBorderColorIndex,
              onSelected: (index) {
                setState(() {
                  _pausedBorderColorIndex = index;
                });
              },
            ),
          ],
        ),
      ),
      const SizedBox(height: 16),
      _SectionCard(
        title: 'Strings And Direction',
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _LabeledControl(
              label: 'Language preset',
              child: SegmentedButton<ShowcaseLanguage>(
                segments: const [
                  ButtonSegment(
                    value: ShowcaseLanguage.english,
                    label: Text('English'),
                  ),
                  ButtonSegment(
                    value: ShowcaseLanguage.hebrew,
                    label: Text('Hebrew'),
                  ),
                ],
                selected: <ShowcaseLanguage>{_language},
                onSelectionChanged: (selection) {
                  setState(() {
                    _language = selection.first;
                  });
                },
              ),
            ),
            const SizedBox(height: 12),
            _LabeledControl(
              label: 'Scanner text direction',
              child: SegmentedButton<TextDirection?>(
                segments: const [
                  ButtonSegment<TextDirection?>(
                    value: null,
                    label: Text('System'),
                  ),
                  ButtonSegment<TextDirection?>(
                    value: TextDirection.ltr,
                    label: Text('LTR'),
                  ),
                  ButtonSegment<TextDirection?>(
                    value: TextDirection.rtl,
                    label: Text('RTL'),
                  ),
                ],
                selected: <TextDirection?>{_scannerTextDirection},
                onSelectionChanged: (selection) {
                  setState(() {
                    _scannerTextDirection = selection.first;
                  });
                },
              ),
            ),
            const SizedBox(height: 12),
            Text(
              'Current effective direction: ${isRtl ? 'RTL' : 'LTR'}',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ],
        ),
      ),
      const SizedBox(height: 16),
      _SectionCard(
        title: 'Allowed Formats',
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: FormatPreset.values.map((preset) {
                return ChoiceChip(
                  label: Text(_formatPresetLabel(preset)),
                  selected: _formatPreset == preset,
                  onSelected: (_) {
                    setState(() {
                      _formatPreset = preset;
                    });
                  },
                );
              }).toList(),
            ),
            const SizedBox(height: 12),
            Text(_formatSummary, style: Theme.of(context).textTheme.bodyMedium),
          ],
        ),
      ),
      const SizedBox(height: 16),
      _SectionCard(
        title: 'Native UI',
        child: Column(
          children: [
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _showFlashButton,
              title: const Text('Show flash button'),
              onChanged: (value) {
                setState(() {
                  _showFlashButton = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _showCameraSwitchButton,
              title: const Text('Show camera switch button'),
              onChanged: (value) {
                setState(() {
                  _showCameraSwitchButton = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _initialTorchEnabled,
              title: const Text('Start with torch enabled'),
              onChanged: (value) {
                setState(() {
                  _initialTorchEnabled = value;
                });
              },
            ),
            const SizedBox(height: 12),
            _LabeledControl(
              label: 'Initial camera lens',
              child: SegmentedButton<BarcodeCameraLens>(
                segments: const [
                  ButtonSegment(
                    value: BarcodeCameraLens.back,
                    label: Text('Back'),
                  ),
                  ButtonSegment(
                    value: BarcodeCameraLens.front,
                    label: Text('Front'),
                  ),
                ],
                selected: <BarcodeCameraLens>{_initialCameraLens},
                onSelectionChanged: (selection) {
                  setState(() {
                    _initialCameraLens = selection.first;
                  });
                },
              ),
            ),
          ],
        ),
      ),
      const SizedBox(height: 16),
      _SectionCard(
        title: 'Scan Window',
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _SliderField(
              label: 'Width factor',
              value: _scanWindowWidthFactor,
              min: 0.35,
              max: 0.85,
              onChanged: (value) {
                setState(() {
                  _scanWindowWidthFactor = value;
                });
              },
            ),
            _SliderField(
              label: 'Height factor',
              value: _scanWindowHeightFactor,
              min: 0.35,
              max: 0.85,
              onChanged: (value) {
                setState(() {
                  _scanWindowHeightFactor = value;
                });
              },
            ),
            _SliderField(
              label: 'Corner radius',
              value: _scanWindowCornerRadius,
              min: 0,
              max: 36,
              divisions: 18,
              onChanged: (value) {
                setState(() {
                  _scanWindowCornerRadius = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _scanWindowEnabled,
              title: const Text('Enable scan window'),
              subtitle: const Text(
                'Turn this off to scan across the full preview with no scan-box UI.',
              ),
              onChanged: (value) {
                setState(() {
                  _scanWindowEnabled = value;
                });
              },
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              children: [
                OutlinedButton(
                  onPressed: () {
                    setState(() {
                      _scanWindowWidthFactor = 0.58;
                      _scanWindowHeightFactor = 0.58;
                    });
                  },
                  child: const Text('Square default'),
                ),
                OutlinedButton(
                  onPressed: () {
                    setState(() {
                      _scanWindowWidthFactor = 0.62;
                      _scanWindowHeightFactor = 0.42;
                    });
                  },
                  child: const Text('Wide barcode window'),
                ),
              ],
            ),
          ],
        ),
      ),
      const SizedBox(height: 16),
      _SectionCard(
        title: 'Status Bar And Chrome',
        child: Column(
          children: [
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _statusBarTransparent,
              title: const Text('Transparent status bar'),
              onChanged: (value) {
                setState(() {
                  _statusBarTransparent = value;
                });
              },
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              value: _appBarTransparent,
              title: const Text('Transparent app bar'),
              onChanged: (value) {
                setState(() {
                  _appBarTransparent = value;
                });
              },
            ),
            const SizedBox(height: 12),
            _LabeledControl(
              label: 'Status bar icon brightness',
              child:
                  SegmentedButton<FlutterBarcodeScannerStatusBarIconBrightness>(
                    segments: const [
                      ButtonSegment(
                        value:
                            FlutterBarcodeScannerStatusBarIconBrightness.light,
                        label: Text('Light'),
                      ),
                      ButtonSegment(
                        value:
                            FlutterBarcodeScannerStatusBarIconBrightness.dark,
                        label: Text('Dark'),
                      ),
                    ],
                    selected: <FlutterBarcodeScannerStatusBarIconBrightness>{
                      _statusBarIconBrightness,
                    },
                    onSelectionChanged: (selection) {
                      setState(() {
                        _statusBarIconBrightness = selection.first;
                      });
                    },
                  ),
            ),
            const SizedBox(height: 12),
            _ColorChoices(
              label: 'App bar color',
              colors: _appBarColors,
              selectedIndex: _appBarColorIndex,
              onSelected: (index) {
                setState(() {
                  _appBarColorIndex = index;
                });
              },
            ),
            const SizedBox(height: 12),
            _LabeledControl(
              label: 'Overlay intensity',
              child: Wrap(
                spacing: 8,
                children: List.generate(_overlayOptions.length, (index) {
                  final opacity = _overlayOptions[index];
                  return ChoiceChip(
                    label: Text('${(opacity * 100).round()}%'),
                    selected: _overlayOpacityIndex == index,
                    onSelected: (_) {
                      setState(() {
                        _overlayOpacityIndex = index;
                      });
                    },
                  );
                }),
              ),
            ),
          ],
        ),
      ),
    ];
  }

  List<Widget> _buildResultsSections(
    BuildContext context,
    ColorScheme scheme,
    bool isRtl,
  ) {
    return [
      if (_showEmbeddedScanner) ...[
        _SectionCard(
          title: 'Embedded Scanner Widget',
          child: Column(
            children: [
              SizedBox(
                height: 360,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(18),
                  child: FlutterBarcodeScannerView(
                    config: _buildConfig(),
                    widgetConfig: _buildWidgetConfig(),
                    controller: _embeddedController,
                    autoStart: _embeddedAutoStart,
                    autoPauseOnScan: _embeddedAutoPauseOnScan,
                    onScan: _recordResult,
                  ),
                ),
              ),
              const SizedBox(height: 12),
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: [
                  OutlinedButton.icon(
                    onPressed: _embeddedController.startCamera,
                    icon: const Icon(Icons.videocam_outlined),
                    label: const Text('Start'),
                  ),
                  OutlinedButton.icon(
                    onPressed: _embeddedController.stopCamera,
                    icon: const Icon(Icons.videocam_off_outlined),
                    label: const Text('Stop'),
                  ),
                  OutlinedButton.icon(
                    onPressed: _embeddedController.pauseDetection,
                    icon: const Icon(Icons.pause_circle_outline),
                    label: const Text('Pause detection'),
                  ),
                  OutlinedButton.icon(
                    onPressed: _embeddedController.resumeDetection,
                    icon: const Icon(Icons.play_circle_outline),
                    label: const Text('Resume detection'),
                  ),
                ],
              ),
            ],
          ),
        ),
        const SizedBox(height: 16),
      ],
      _SectionCard(
        title: 'Active Config Preview',
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _ConfigLine(label: 'Direction', value: isRtl ? 'RTL' : 'LTR'),
            _ConfigLine(label: 'Formats', value: _formatSummary),
            _ConfigLine(
              label: 'Window',
              value: _scanWindowEnabled
                  ? '${_scanWindowWidthFactor.toStringAsFixed(2)} x ${_scanWindowHeightFactor.toStringAsFixed(2)}'
                  : 'Full preview',
            ),
            _ConfigLine(
              label: 'Radius',
              value: _scanWindowCornerRadius.toStringAsFixed(0),
            ),
            _ConfigLine(
              label: 'Flash',
              value:
                  '${_showFlashButton ? 'shown' : 'hidden'} / ${_initialTorchEnabled ? 'on' : 'off'}',
            ),
            _ConfigLine(
              label: 'Camera switch',
              value: _showCameraSwitchButton ? 'shown' : 'hidden',
            ),
            _ConfigLine(
              label: 'Status bar',
              value: _statusBarTransparent
                  ? 'transparent'
                  : _statusBarIconBrightness.name,
            ),
            _ConfigLine(
              label: 'App bar',
              value: _appBarTransparent ? 'transparent' : 'colored',
            ),
            _ConfigLine(
              label: 'Language',
              value: _language == ShowcaseLanguage.hebrew
                  ? 'Hebrew'
                  : 'English',
            ),
          ],
        ),
      ),
      const SizedBox(height: 16),
      _SectionCard(
        title: 'Last Result',
        child: _lastResult == null
            ? const Text('No scan result yet.')
            : Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _ConfigLine(label: 'Type', value: _lastResult!.type.name),
                  _ConfigLine(
                    label: 'Raw value',
                    value: _lastResult!.rawValue.isEmpty
                        ? '—'
                        : _lastResult!.rawValue,
                  ),
                  _ConfigLine(
                    label: 'Format',
                    value: _lastResult!.format.nativeValue,
                  ),
                  if (_lastResult!.errorMessage != null)
                    _ConfigLine(
                      label: 'Error',
                      value: _lastResult!.errorMessage!,
                    ),
                ],
              ),
      ),
      const SizedBox(height: 16),
      _SectionCard(
        title: 'Recent History',
        child: _scanHistory.isEmpty
            ? const Text('No scans captured in this session.')
            : Column(
                children: _scanHistory.map((result) {
                  final color = result.isCancelled
                      ? scheme.secondary
                      : result.isBarcode
                      ? scheme.primary
                      : scheme.error;
                  return ListTile(
                    dense: true,
                    contentPadding: EdgeInsets.zero,
                    leading: Icon(
                      result.isBarcode
                          ? Icons.qr_code_2
                          : result.isCancelled
                          ? Icons.close
                          : Icons.warning_rounded,
                      color: color,
                    ),
                    title: Text(
                      result.rawValue.isEmpty
                          ? result.type.name
                          : result.rawValue,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    subtitle: Text(result.format.nativeValue),
                  );
                }).toList(),
              ),
      ),
    ];
  }

  Future<void> _startScannerFlow() async {
    if (_isLaunchingScanner) {
      return;
    }

    setState(() {
      _isLaunchingScanner = true;
    });

    try {
      while (mounted) {
        await _prepareForScannerPresentation();
        if (!mounted) {
          return;
        }
        final result = await FlutterBarcodeScanner.scan(_buildConfig());
        if (!mounted) {
          return;
        }

        if (result != null) {
          _recordResult(result);
        }

        if (!_isContinuousLoop || result == null || result.isCancelled) {
          break;
        }

        if (_isContinuousLoop) {
          await Future.delayed(const Duration(milliseconds: 500));
        }
      }
    } finally {
      if (mounted) {
        setState(() {
          _isLaunchingScanner = false;
        });
      }
    }
  }

  void _recordResult(FlutterBarcodeScanResult result) {
    setState(() {
      _lastResult = result;
      _scanHistory.insert(0, result);
      if (_scanHistory.length > 8) {
        _scanHistory.removeLast();
      }
    });
  }

  Future<void> _prepareForScannerPresentation() async {
    await Future<void>.delayed(Duration.zero);
    await WidgetsBinding.instance.endOfFrame;
    await WidgetsBinding.instance.endOfFrame;
    await Future<void>.delayed(const Duration(milliseconds: 64));
  }

  FlutterBarcodeScannerConfig _buildConfig() {
    final appBarColor = _appBarColors[_appBarColorIndex];
    final overlayOpacity = _overlayOptions[_overlayOpacityIndex];
    final strings = switch (_language) {
      ShowcaseLanguage.english => const FlutterBarcodeScannerStrings(
        title: 'Scan Ticket',
        close: 'Close',
        flashOn: 'Flash on',
        flashOff: 'Flash off',
        switchCamera: 'Switch camera',
        cameraPermissionRequired: 'Camera permission is required',
        cameraUnavailable: 'Camera unavailable',
      ),
      ShowcaseLanguage.hebrew => const FlutterBarcodeScannerStrings(
        title: 'מצלמה',
        close: 'סגור',
        flashOn: 'הפעל פלאש',
        flashOff: 'כבה פלאש',
        switchCamera: 'החלף מצלמה',
        cameraPermissionRequired: 'נדרשת הרשאת מצלמה',
        cameraUnavailable: 'המצלמה אינה זמינה',
      ),
    };

    return const FlutterBarcodeScannerConfig().copyWith(
      allowedFormats: _allowedFormats,
      strings: strings,
      scanWindow: FlutterBarcodeScannerScanWindow(
        enabled: _scanWindowEnabled,
        widthFactor: _scanWindowWidthFactor,
        heightFactor: _scanWindowHeightFactor,
        cornerRadius: _scanWindowCornerRadius,
      ),
      uiConfig: FlutterBarcodeScannerUiConfig(
        showFlashButton: _showFlashButton,
        showCameraSwitchButton: _showCameraSwitchButton,
        initialCameraLens: _initialCameraLens,
        initialTorchEnabled: _initialTorchEnabled,
      ),
      statusBarStyle: FlutterBarcodeScannerStatusBarStyle(
        isTransparent: _statusBarTransparent,
        backgroundColor: _statusBarTransparent ? null : appBarColor,
        iconBrightness: _statusBarIconBrightness,
      ),
      textDirection: _scannerTextDirection,
      appBarTransparent: _appBarTransparent,
      appBarBackgroundColor: appBarColor,
      appBarForegroundColor: Colors.white,
      overlayColor: Colors.black.withValues(alpha: overlayOpacity),
    );
  }

  FlutterBarcodeScannerWidgetConfig _buildWidgetConfig() {
    return FlutterBarcodeScannerWidgetConfig(
      autoRequestCameraPermission: _embeddedAutoRequestCameraPermission,
      freezePreviewWhenPaused: _embeddedFreezePreviewWhenPaused,
      showPauseResumeButton: _embeddedShowPauseResumeButton,
      pausedScanWindowBorderColor: _pausedBorderColors[_pausedBorderColorIndex],
    );
  }

  Set<FlutterBarcodeScannerFormat> get _allowedFormats {
    switch (_formatPreset) {
      case FormatPreset.all:
        return FlutterBarcodeScannerFormats.all;
      case FormatPreset.qrAndCode128:
        return const {
          FlutterBarcodeScannerFormat.qrCode,
          FlutterBarcodeScannerFormat.code128,
        };
      case FormatPreset.twoDimensionalOnly:
        return FlutterBarcodeScannerFormats.twoDimensional;
      case FormatPreset.oneDimensionalOnly:
        return FlutterBarcodeScannerFormats.oneDimensional;
    }
  }

  String get _formatSummary {
    switch (_formatPreset) {
      case FormatPreset.all:
        return 'All supported formats';
      case FormatPreset.qrAndCode128:
        return 'QR Code + CODE_128';
      case FormatPreset.twoDimensionalOnly:
        return 'QR + PDF417 + Data Matrix + Aztec';
      case FormatPreset.oneDimensionalOnly:
        return 'CODE_128 + CODE_39 + CODE_93 + EAN/UPC + ITF';
    }
  }

  String _formatPresetLabel(FormatPreset preset) {
    switch (preset) {
      case FormatPreset.all:
        return 'All';
      case FormatPreset.qrAndCode128:
        return 'QR + 128';
      case FormatPreset.twoDimensionalOnly:
        return '2D only';
      case FormatPreset.oneDimensionalOnly:
        return '1D only';
    }
  }
}

class _SectionCard extends StatelessWidget {
  const _SectionCard({required this.title, required this.child});

  final String title;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleLarge),
            const SizedBox(height: 12),
            child,
          ],
        ),
      ),
    );
  }
}

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

  final String label;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label, style: Theme.of(context).textTheme.labelLarge),
        const SizedBox(height: 8),
        child,
      ],
    );
  }
}

class _SliderField extends StatelessWidget {
  const _SliderField({
    required this.label,
    required this.value,
    required this.min,
    required this.max,
    required this.onChanged,
    this.divisions,
  });

  final String label;
  final double value;
  final double min;
  final double max;
  final int? divisions;
  final ValueChanged<double> onChanged;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('$label: ${value.toStringAsFixed(2)}'),
        Slider(
          value: value,
          min: min,
          max: max,
          divisions: divisions,
          label: value.toStringAsFixed(2),
          onChanged: onChanged,
        ),
      ],
    );
  }
}

class _ColorChoices extends StatelessWidget {
  const _ColorChoices({
    required this.label,
    required this.colors,
    required this.selectedIndex,
    required this.onSelected,
  });

  final String label;
  final List<Color> colors;
  final int selectedIndex;
  final ValueChanged<int> onSelected;

  @override
  Widget build(BuildContext context) {
    return _LabeledControl(
      label: label,
      child: Wrap(
        spacing: 10,
        runSpacing: 10,
        children: List.generate(colors.length, (index) {
          final color = colors[index];
          final selected = index == selectedIndex;
          return InkWell(
            onTap: () => onSelected(index),
            borderRadius: BorderRadius.circular(24),
            child: Container(
              width: 40,
              height: 40,
              decoration: BoxDecoration(
                color: color,
                shape: BoxShape.circle,
                border: Border.all(
                  color: selected
                      ? Theme.of(context).colorScheme.primary
                      : Colors.transparent,
                  width: 3,
                ),
              ),
            ),
          );
        }),
      ),
    );
  }
}

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

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 110,
            child: Text(label, style: Theme.of(context).textTheme.labelLarge),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }
}