• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / inventree-app / 15482909765

06 Jun 2025 04:36AM UTC coverage: 1.615% (-6.5%) from 8.127%
15482909765

Pull #651

github

web-flow
Merge 39ba274a2 into c3f390edd
Pull Request #651: Barcode scan refactor

1 of 79 new or added lines in 3 files covered. (1.27%)

2 existing lines in 1 file now uncovered.

757 of 46877 relevant lines covered (1.61%)

0.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/lib/barcode/camera_controller.dart
1
import "dart:math";
2
import "dart:typed_data";
3

4
import "package:camera/camera.dart";
5
import "package:flutter/material.dart";
6
import "package:flutter_speed_dial/flutter_speed_dial.dart";
7
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
8
import "package:inventree/app_colors.dart";
9
import "package:inventree/inventree/sentry.dart";
10
import "package:inventree/preferences.dart";
11
import "package:inventree/widget/snacks.dart";
12
import "package:mobile_scanner/mobile_scanner.dart";
13
import "package:one_context/one_context.dart";
14
import "package:wakelock_plus/wakelock_plus.dart";
15

16
import "package:inventree/l10.dart";
17

18
import "package:inventree/barcode/handler.dart";
19
import "package:inventree/barcode/controller.dart";
20

21
/*
22
 * Barcode controller which uses the device's camera to scan barcodes.
23
 * Under the hood it uses the qr_code_scanner package.
24
 */
25
class CameraBarcodeController extends InvenTreeBarcodeController {
26
  const CameraBarcodeController(BarcodeHandler handler, {Key? key})
×
27
      : super(handler, key: key);
×
28

29
  @override
×
30
  State<StatefulWidget> createState() => _CameraBarcodeControllerState();
×
31
}
32

33
class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
34
  _CameraBarcodeControllerState() : super();
×
35

36
  bool flash_status = false;
37

38
  int scan_delay = 500;
39
  bool single_scanning = false;
40
  bool scanning_paused = false;
41
  bool multiple_barcodes = false;
42

43
  String scanned_code = "";
44

45
  final MobileScannerController controller = MobileScannerController(
46
    autoZoom: true
47
  );
48

UNCOV
49
  @override
×
50
  void initState() {
51
    super.initState();
×
52
    _loadSettings();
×
53
    WakelockPlus.enable();
×
54
  }
55

56
  @override
×
57
  void dispose() {
58
    super.dispose();
×
59
    WakelockPlus.disable();
×
60
  }
61

62
  /*
63
   * Load the barcode scanning settings
64
   */
65
  Future<void> _loadSettings() async {
×
66
    bool _single = await InvenTreeSettingsManager()
×
67
        .getBool(INV_BARCODE_SCAN_SINGLE, false);
×
68

69
    int _delay = await InvenTreeSettingsManager()
×
70
        .getValue(INV_BARCODE_SCAN_DELAY, 500) as int;
×
71

72
    if (mounted) {
×
73
      setState(() {
×
74
        scan_delay = _delay;
×
75
        single_scanning = _single;
×
76
        scanning_paused = false;
×
77
      });
78
    }
79
  }
80

81
  @override
×
82
  Future<void> pauseScan() async {
83

84
    if (mounted) {
×
85
      setState(() {
×
86
        scanning_paused = true;
×
87
      });
88
    }
89
  }
90

91
  @override
×
92
  Future<void> resumeScan() async {
93

NEW
94
    controller.start();
×
95

96
    if (mounted) {
×
97
      setState(() {
×
98
        scanning_paused = false;
×
99
      });
100
    }
101
  }
102

103
  /*
104
   * Callback function when a barcode is scanned
105
   */
NEW
106
  Future<void> onScanSuccess(BarcodeCapture result) async {
×
NEW
107
    if (!mounted || scanning_paused) {
×
108
      return;
109
    }
110

111
    // TODO: Display outline of barcodes on the screen?
112

NEW
113
    if (result.barcodes.isEmpty) {
×
NEW
114
      setState(() {
×
NEW
115
        multiple_barcodes = false;
×
116
      });
117
    }
NEW
118
    else if (result.barcodes.length > 1) {
×
NEW
119
      setState(() {
×
NEW
120
        multiple_barcodes = true;
×
121
      });
122
      return;
123
    } else {
NEW
124
      setState(() {
×
NEW
125
        multiple_barcodes = false;
×
126
      });
127
    }
128

NEW
129
    Uint8List rawData = result.barcodes.first.rawBytes ?? Uint8List(0);
×
130

131
    String barcode;
132

NEW
133
    if (rawData.isNotEmpty) {
×
UNCOV
134
      final buffer = StringBuffer();
×
135

NEW
136
      for (int ii = 0; ii < rawData.length; ii++) {
×
NEW
137
        buffer.writeCharCode(rawData[ii]);
×
138
      }
139

140
      barcode = buffer.toString();
×
141

NEW
142
      print(barcode);
×
143
    } else {
144
      // Fall back to text value
NEW
145
      barcode = result.barcodes.first.rawValue ?? "";
×
146
    }
147

NEW
148
    if (barcode.isEmpty) {
×
149
      // TODO: Error message "empty barcode"
150
      return;
151
    }
152

NEW
153
    setState(() {
×
NEW
154
      scanned_code = barcode;
×
155
    });
156

NEW
157
    pauseScan();
×
158

NEW
159
    await handleBarcodeData(barcode).then((_) {
×
NEW
160
      if (!single_scanning && mounted) {
×
NEW
161
        resumeScan();
×
162
      }
163
    });
164

NEW
165
    resumeScan();
×
166

NEW
167
    if (mounted) {
×
NEW
168
      setState(() {
×
NEW
169
        scanned_code = "";
×
NEW
170
        multiple_barcodes = false;
×
171
      });
172
    }
173
  }
174

175
  void onControllerCreated(CameraController? controller, Exception? error) {
×
176
    if (error != null) {
177
      sentryReportError(
×
178
        "CameraBarcodeController.onControllerCreated",
179
        error,
180
        null
181
      );
182
    }
183

184
    if (controller == null) {
185
      showSnackIcon(
×
186
        L10().cameraCreationError,
×
187
        icon: TablerIcons.camera_x,
188
        success: false
189
      );
190

191
      if (OneContext.hasContext) {
×
192
        Navigator.pop(OneContext().context!);
×
193
      }
194
    }
195
  }
196

NEW
197
  Widget BarcodeOverlay(BuildContext context) {
×
198

NEW
199
    final Size screenSize = MediaQuery.of(context).size;
×
NEW
200
    final double width = screenSize.width;
×
NEW
201
    final double height = screenSize.height;
×
202

NEW
203
    final double D = min(width, height) * 0.8;
×
204

205
    // Color for the barcode scan?
NEW
206
    Color overlayColor = COLOR_ACTION;
×
207

NEW
208
    if (multiple_barcodes) {
×
209
      overlayColor = COLOR_DANGER;
NEW
210
    } else if (scanned_code.isNotEmpty) {
×
211
      overlayColor = COLOR_SUCCESS;
NEW
212
    } else if (scanning_paused) {
×
213
      overlayColor = COLOR_WARNING;
214
    }
215

NEW
216
    return Stack(
×
NEW
217
      children: [
×
NEW
218
        Center(
×
NEW
219
          child: Container(
×
220
            width: D,
221
            height: D,
NEW
222
            decoration: BoxDecoration(
×
NEW
223
              border: Border.all(
×
224
                color: overlayColor,
225
                width: 4,
226
              ),
227
            ),
228
          )
229
        )
230
      ]
231
    );
232
  }
233

234
  /*
235
   * Build the barcode reader widget
236
   */
237
  Widget BarcodeReader(BuildContext context) {
×
238

NEW
239
    final Size screenSize = MediaQuery.of(context).size;
×
NEW
240
    final double width = screenSize.width;
×
NEW
241
    final double height = screenSize.height;
×
242

NEW
243
    final double D = min(width, height) * 0.8;
×
244

NEW
245
    return MobileScanner(
×
NEW
246
      controller: controller,
×
NEW
247
      overlayBuilder: (context, constraints) {
×
NEW
248
        return BarcodeOverlay(context);
×
249
      },
NEW
250
      scanWindow: Rect.fromCenter(
×
NEW
251
        center: Offset(width / 2, height / 2),
×
252
        width: D,
253
        height: D
254
      ),
NEW
255
      onDetect: (result) {
×
NEW
256
        onScanSuccess(result);
×
257
      },
258
    );
259
  }
260

261
  Widget topCenterOverlay() {
×
262
    return SafeArea(
×
263
      child: Align(
×
264
        alignment: Alignment.topCenter,
265
        child: Padding(
×
266
          padding: EdgeInsets.only(
×
267
            left: 10,
268
            right: 10,
269
            top: 75,
270
            bottom: 10
271
          ),
272
          child: Text(
×
273
            widget.handler.getOverlayText(context),
×
274
            style: TextStyle(
×
275
              color: Colors.white,
276
              fontSize: 16,
277
              fontWeight: FontWeight.bold
278
            )
279
          )
280
        )
281
      )
282
    );
283
  }
284

285
  Widget bottomCenterOverlay() {
×
286

287
    String info_text = scanning_paused ? L10().barcodeScanPaused : L10().barcodeScanPause;
×
288

289
    String text = scanned_code.isNotEmpty ? scanned_code : info_text;
×
290

291
    if (text.length > 50) {
×
292
      text = text.substring(0, 50) + "...";
×
293
    }
294

295
    return SafeArea(
×
296
      child: Align(
×
297
        alignment: Alignment.bottomCenter,
298
        child: Padding(
×
299
          padding: EdgeInsets.only(
×
300
            left: 10,
301
            right: 10,
302
            top: 10,
303
            bottom: 75
304
          ),
305
          child: Text(
×
306
              text,
307
              textAlign: TextAlign.center,
308
              style: TextStyle(
×
309
                color: Colors.white,
310
                fontSize: 16,
311
                fontWeight: FontWeight.bold
312
              )
313
          ),
314
        )
315
      )
316
    );
317
  }
318

NEW
319
  Widget? buildActions(BuildContext context) {
×
320

NEW
321
    List<SpeedDialChild> actions = [
×
NEW
322
      SpeedDialChild(
×
NEW
323
        child: Icon(flash_status ? TablerIcons.bulb_off : TablerIcons.bulb),
×
NEW
324
        label: L10().toggleTorch,
×
NEW
325
        onTap: () async {
×
NEW
326
          controller.toggleTorch();
×
NEW
327
          if (mounted) {
×
NEW
328
            setState(() {
×
NEW
329
              flash_status = !flash_status;
×
330
            });
331
          }
332
        }
333
      ),
NEW
334
      SpeedDialChild(
×
NEW
335
        child: Icon(TablerIcons.camera),
×
NEW
336
        label: L10().switchCamera,
×
NEW
337
        onTap: () async {
×
NEW
338
          controller.switchCamera();
×
339
        }
340
      )
341
    ];
342

NEW
343
    return SpeedDial(
×
344
      icon: Icons.more_horiz,
345
      children: actions,
346
    );
347
  }
348

349
  @override
×
350
  Widget build(BuildContext context) {
351

352
    return Scaffold(
×
353
      appBar: AppBar(
×
354
        backgroundColor: COLOR_APP_BAR,
×
355
        title: Text(L10().scanBarcode),
×
356
      ),
NEW
357
      floatingActionButton: buildActions(context),
×
358
      body: GestureDetector(
×
359
        onTap: () async {
×
NEW
360
          if (mounted) {
×
NEW
361
            setState(() {
×
362
              // Toggle the 'scan paused' state
NEW
363
              scanning_paused = !scanning_paused;
×
364
            });
365
          }
366
        },
367
        child: Stack(
×
368
          children: <Widget>[
×
369
            Column(
×
370
              children: [
×
371
                Expanded(
×
372
                    child: BarcodeReader(context)
×
373
                ),
374
              ],
375
            ),
376
            topCenterOverlay(),
×
NEW
377
            bottomCenterOverlay()
×
378
          ],
379
        ),
380
      ),
381
    );
382
  }
383

384
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc