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

inventree / inventree-app / 19514260907

19 Nov 2025 07:44PM UTC coverage: 1.469% (-0.001%) from 1.47%
19514260907

push

github

web-flow
Add zoom slider to barcode scanning (#725)

0 of 29 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

768 of 52269 relevant lines covered (1.47%)

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

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

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

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

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

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

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

35
  bool flash_status = false;
36

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

42
  String scanned_code = "";
43

44
  double zoomFactor = 0.0;
45

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

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

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

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

73
    int _delay =
74
        await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500)
×
75
            as int;
76

77
    if (mounted) {
×
78
      setState(() {
×
79
        scan_delay = _delay;
×
80
        single_scanning = _single;
×
81
        scanning_paused = false;
×
82
      });
83
    }
84
  }
85

86
  @override
×
87
  Future<void> pauseScan() async {
88
    if (mounted) {
×
89
      setState(() {
×
90
        scanning_paused = true;
×
91
      });
92
    }
93
  }
94

95
  @override
×
96
  Future<void> resumeScan() async {
97
    controller.start();
×
98

99
    if (mounted) {
×
100
      setState(() {
×
101
        scanning_paused = false;
×
102
      });
103
    }
104
  }
105

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

114
    // TODO: Display outline of barcodes on the screen?
115

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

131
    String barcode = result.barcodes.first.rawValue ?? "";
×
132

133
    if (barcode.isEmpty) {
×
134
      // TODO: Error message "empty barcode"
135
      return;
136
    }
137

138
    setState(() {
×
139
      scanned_code = barcode;
×
140
    });
141

142
    pauseScan();
×
143

144
    await handleBarcodeData(barcode).then((_) {
×
145
      if (!single_scanning && mounted) {
×
146
        resumeScan();
×
147
      }
148
    });
149

150
    resumeScan();
×
151

152
    if (mounted) {
×
153
      setState(() {
×
154
        scanned_code = "";
×
155
        multiple_barcodes = false;
×
156
      });
157
    }
158
  }
159

160
  void onControllerCreated(CameraController? controller, Exception? error) {
×
161
    if (error != null) {
162
      sentryReportError(
×
163
        "CameraBarcodeController.onControllerCreated",
164
        error,
165
        null,
166
      );
167
    }
168

169
    if (controller == null) {
170
      showSnackIcon(
×
171
        L10().cameraCreationError,
×
172
        icon: TablerIcons.camera_x,
173
        success: false,
174
      );
175

176
      if (OneContext.hasContext) {
×
177
        Navigator.pop(OneContext().context!);
×
178
      }
179
    }
180
  }
181

182
  Widget BarcodeOverlay(BuildContext context) {
×
183
    final Size screenSize = MediaQuery.of(context).size;
×
184
    final double width = screenSize.width;
×
185
    final double height = screenSize.height;
×
186

187
    final double D = min(width, height) * 0.8;
×
188

189
    // Color for the barcode scan?
190
    Color overlayColor = COLOR_ACTION;
×
191

192
    if (multiple_barcodes) {
×
193
      overlayColor = COLOR_DANGER;
194
    } else if (scanned_code.isNotEmpty) {
×
195
      overlayColor = COLOR_SUCCESS;
196
    } else if (scanning_paused) {
×
197
      overlayColor = COLOR_WARNING;
198
    }
199

200
    return Stack(
×
201
      children: [
×
202
        Center(
×
203
          child: Container(
×
204
            width: D,
205
            height: D,
206
            decoration: BoxDecoration(
×
207
              border: Border.all(color: overlayColor, width: 4),
×
208
            ),
209
          ),
210
        ),
211
      ],
212
    );
213
  }
214

215
  /*
216
   * Build the barcode reader widget
217
   */
218
  Widget BarcodeReader(BuildContext context) {
×
219
    final Size screenSize = MediaQuery.of(context).size;
×
220
    final double width = screenSize.width;
×
221
    final double height = screenSize.height;
×
222

223
    final double D = min(width, height) * 0.8;
×
224

225
    return MobileScanner(
×
226
      controller: controller,
×
227
      overlayBuilder: (context, constraints) {
×
228
        return BarcodeOverlay(context);
×
229
      },
230
      scanWindow: Rect.fromCenter(
×
231
        center: Offset(width / 2, height / 2),
×
232
        width: D,
233
        height: D,
234
      ),
235
      onDetect: (result) {
×
236
        onScanSuccess(result);
×
237
      },
238
    );
239
  }
240

241
  Widget topCenterOverlay() {
×
242
    return SafeArea(
×
243
      child: Align(
×
244
        alignment: Alignment.topCenter,
245
        child: Padding(
×
246
          padding: EdgeInsets.only(left: 10, right: 10, top: 75, bottom: 10),
×
247
          child: Text(
×
248
            widget.handler.getOverlayText(context),
×
249
            style: TextStyle(
×
250
              color: Colors.white,
251
              fontSize: 16,
252
              fontWeight: FontWeight.bold,
253
            ),
254
          ),
255
        ),
256
      ),
257
    );
258
  }
259

260
  Widget bottomCenterOverlay() {
×
261
    String info_text = scanning_paused
×
262
        ? L10().barcodeScanPaused
×
263
        : L10().barcodeScanPause;
×
264

265
    String text = scanned_code.isNotEmpty ? scanned_code : info_text;
×
266

267
    if (text.length > 50) {
×
268
      text = text.substring(0, 50) + "...";
×
269
    }
270

271
    return SafeArea(
×
272
      child: Align(
×
273
        alignment: Alignment.bottomCenter,
274
        child: Padding(
×
275
          padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 75),
×
276
          child: Text(
×
277
            text,
278
            textAlign: TextAlign.center,
279
            style: TextStyle(
×
280
              color: Colors.white,
281
              fontSize: 16,
282
              fontWeight: FontWeight.bold,
283
            ),
284
          ),
285
        ),
286
      ),
287
    );
288
  }
289

290
  Widget? buildActions(BuildContext context) {
×
291
    List<SpeedDialChild> actions = [
×
292
      SpeedDialChild(
×
293
        child: Icon(flash_status ? TablerIcons.bulb_off : TablerIcons.bulb),
×
294
        label: L10().toggleTorch,
×
295
        onTap: () async {
×
296
          controller.toggleTorch();
×
297
          if (mounted) {
×
298
            setState(() {
×
299
              flash_status = !flash_status;
×
300
            });
301
          }
302
        },
303
      ),
304
      SpeedDialChild(
×
305
        child: Icon(TablerIcons.camera),
×
306
        label: L10().switchCamera,
×
307
        onTap: () async {
×
308
          controller.switchCamera();
×
309
        },
310
      ),
311
    ];
312

313
    return SpeedDial(icon: Icons.more_horiz, children: actions);
×
314
  }
315

NEW
316
  Widget zoomSlider() {
×
NEW
317
    return Positioned(
×
318
      left: 0,
319
      right: 0,
320
      bottom: 16,
NEW
321
      child: Center(
×
NEW
322
        child: Container(
×
323
          width: 225,
324
          height: 56,
NEW
325
          decoration: BoxDecoration(
×
NEW
326
            color: Colors.black.withValues(alpha: 0.3),
×
NEW
327
            borderRadius: BorderRadius.circular(28),
×
328
          ),
NEW
329
          padding: EdgeInsets.symmetric(horizontal: 16),
×
NEW
330
          child: Row(
×
NEW
331
            children: [
×
NEW
332
              Icon(TablerIcons.zoom_out, color: Colors.white, size: 20),
×
NEW
333
              Expanded(
×
NEW
334
                child: Slider(
×
NEW
335
                  value: zoomFactor,
×
336
                  min: 0.0,
337
                  max: 1.0,
338
                  activeColor: Colors.white,
NEW
339
                  inactiveColor: Colors.white.withValues(alpha: 0.3),
×
NEW
340
                  onChanged: (value) {
×
NEW
341
                    setState(() {
×
NEW
342
                      zoomFactor = value;
×
NEW
343
                      controller.setZoomScale(value);
×
344
                    });
345
                  },
NEW
346
                  onChangeStart: (value) async {
×
NEW
347
                    if (mounted) {
×
NEW
348
                      setState(() {
×
NEW
349
                        scanning_paused = true;
×
350
                      });
351
                    }
352
                  },
NEW
353
                  onChangeEnd: (value) async {
×
NEW
354
                    if (mounted) {
×
NEW
355
                      setState(() {
×
NEW
356
                        scanning_paused = false;
×
357
                      });
358
                    }
359
                  },
360
                ),
361
              ),
NEW
362
              Icon(TablerIcons.zoom_in, color: Colors.white, size: 20),
×
363
            ],
364
          ),
365
        ),
366
      ),
367
    );
368
  }
369

UNCOV
370
  @override
×
371
  Widget build(BuildContext context) {
372
    return Scaffold(
×
373
      appBar: AppBar(
×
374
        backgroundColor: COLOR_APP_BAR,
375
        title: Text(L10().scanBarcode),
×
376
      ),
377
      floatingActionButton: buildActions(context),
×
378
      body: GestureDetector(
×
379
        onTap: () async {
×
380
          if (mounted) {
×
381
            setState(() {
×
382
              // Toggle the 'scan paused' state
383
              scanning_paused = !scanning_paused;
×
384
            });
385
          }
386
        },
387
        child: Stack(
×
388
          children: <Widget>[
×
389
            Column(children: [Expanded(child: BarcodeReader(context))]),
×
390
            topCenterOverlay(),
×
391
            bottomCenterOverlay(),
×
NEW
392
            zoomSlider(),
×
393
          ],
394
        ),
395
      ),
396
    );
397
  }
398
}
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