
Refactor `ProductScannedComponent`: - Removes redundant full-screen container. - Adjusts padding and image size. - Updates button styles to `FilledButton`. Improve `ScannerPage`: - Correctly updates loading state after product API calls. - Simplifies fake barcode simulation for testing with a hardcoded value.
497 lines
20 KiB
Dart
497 lines
20 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:barcode_scanner/backend/api/api_calls.dart';
|
|
import 'package:barcode_scanner/components/product_scanned_component.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
|
|
class ScannerPage extends ConsumerStatefulWidget {
|
|
const ScannerPage({super.key});
|
|
|
|
@override
|
|
ConsumerState<ConsumerStatefulWidget> createState() => _ScannerPageState();
|
|
}
|
|
|
|
class _ScannerPageState extends ConsumerState<ScannerPage>
|
|
with WidgetsBindingObserver {
|
|
final MobileScannerController mobileScannerController =
|
|
MobileScannerController(
|
|
// required options for the scanner
|
|
);
|
|
|
|
StreamSubscription<Object?>? _subscription;
|
|
bool qrcodeFound = false;
|
|
bool loading = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Start listening to lifecycle changes.
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
// Start listening to the barcode events.
|
|
_subscription = mobileScannerController.barcodes.listen(_handleBarcode);
|
|
|
|
// Finally, start the scanner itself.
|
|
unawaited(mobileScannerController.start());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Stop listening to lifecycle changes.
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
// Stop listening to the barcode events.
|
|
unawaited(_subscription?.cancel());
|
|
_subscription = null;
|
|
// Dispose the widget itself.
|
|
// Finally, dispose of the controller.
|
|
mobileScannerController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
// If the controller is not ready, do not try to start or stop it.
|
|
// Permission dialogs can trigger lifecycle changes before the controller is ready.
|
|
if (!mobileScannerController.value.hasCameraPermission) {
|
|
return;
|
|
}
|
|
|
|
switch (state) {
|
|
case AppLifecycleState.detached:
|
|
case AppLifecycleState.hidden:
|
|
case AppLifecycleState.paused:
|
|
return;
|
|
case AppLifecycleState.resumed:
|
|
// Restart the scanner when the app is resumed.
|
|
// Don't forget to resume listening to the barcode events.
|
|
_subscription = mobileScannerController.barcodes.listen(_handleBarcode);
|
|
|
|
unawaited(mobileScannerController.start());
|
|
case AppLifecycleState.inactive:
|
|
// Stop the scanner when the app is paused.
|
|
// Also stop the barcode events subscription.
|
|
unawaited(_subscription?.cancel());
|
|
_subscription = null;
|
|
unawaited(mobileScannerController.stop());
|
|
}
|
|
}
|
|
|
|
Future<void> _handleBarcode(BarcodeCapture barcodeCapture) async {
|
|
if (qrcodeFound) return;
|
|
if (barcodeCapture.barcodes.isNotEmpty) {
|
|
qrcodeFound = true;
|
|
mobileScannerController.stop();
|
|
final code = barcodeCapture.barcodes.first;
|
|
final qrcodeValue = code.displayValue;
|
|
debugPrint("Code détecté : ${code.displayValue}");
|
|
|
|
if (qrcodeValue == null) {
|
|
debugPrint("Qrcode non valide");
|
|
return;
|
|
}
|
|
setState(() {
|
|
loading = true;
|
|
});
|
|
final product = await ApiCalls.fetchProduct(code.displayValue!);
|
|
if (product != null) {
|
|
setState(() {
|
|
loading = false;
|
|
});
|
|
print('Nom du produit : ${product["product_name"]}');
|
|
print('Marque : ${product["brands"]}');
|
|
print('Image : ${product["image_url"]}');
|
|
//show dialog
|
|
await showDialog(
|
|
barrierDismissible: false,
|
|
context: context,
|
|
builder: (dialogContext) {
|
|
return Dialog(
|
|
elevation: 0,
|
|
insetPadding: EdgeInsets.zero,
|
|
backgroundColor: Colors.transparent,
|
|
alignment: AlignmentDirectional(
|
|
0.0,
|
|
0.0,
|
|
).resolve(Directionality.of(context)),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
FocusScope.of(dialogContext).unfocus();
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
},
|
|
child: Container(
|
|
width: MediaQuery.sizeOf(context).width * 0.9,
|
|
child: ProductScannedComponent(
|
|
img: product["image_url"],
|
|
productName: product["product_name"],
|
|
brands: product["brands"],
|
|
codeScanned: qrcodeValue,
|
|
onRescan: () async {
|
|
Navigator.of(context).pop();
|
|
qrcodeFound = false;
|
|
mobileScannerController.start();
|
|
},
|
|
onDetails: () async {
|
|
Navigator.of(context).pop();
|
|
unawaited(_subscription?.cancel());
|
|
_subscription = null;
|
|
unawaited(mobileScannerController.stop());
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
} else {
|
|
setState(() {
|
|
loading = false;
|
|
});
|
|
print('Aucun produit trouvé.');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _fakeBarcode() async {
|
|
final qrcodeValue = "0737628064502";
|
|
qrcodeFound = true;
|
|
mobileScannerController.stop();
|
|
setState(() {
|
|
loading = true;
|
|
});
|
|
final product = await ApiCalls.fetchProduct(qrcodeValue);
|
|
if (product != null) {
|
|
setState(() {
|
|
loading = false;
|
|
});
|
|
print('Nom du produit : ${product["product_name"]}');
|
|
print('Marque : ${product["brands"]}');
|
|
print('Image : ${product["image_url"]}');
|
|
//show dialog
|
|
await showDialog(
|
|
barrierDismissible: false,
|
|
context: context,
|
|
builder: (dialogContext) {
|
|
return Dialog(
|
|
elevation: 0,
|
|
insetPadding: EdgeInsets.zero,
|
|
backgroundColor: Colors.transparent,
|
|
alignment: AlignmentDirectional(
|
|
0.0,
|
|
0.0,
|
|
).resolve(Directionality.of(context)),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
FocusScope.of(dialogContext).unfocus();
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
},
|
|
child: SizedBox(
|
|
width: MediaQuery.sizeOf(context).width * 0.9,
|
|
child: ProductScannedComponent(
|
|
img: product["image_url"],
|
|
productName: product["product_name"],
|
|
brands: product["brands"],
|
|
codeScanned: qrcodeValue,
|
|
onRescan: () async {
|
|
Navigator.of(context).pop();
|
|
qrcodeFound = false;
|
|
mobileScannerController.start();
|
|
},
|
|
onDetails: () async {
|
|
Navigator.of(context).pop();
|
|
unawaited(_subscription?.cancel());
|
|
_subscription = null;
|
|
unawaited(mobileScannerController.stop());
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
} else {
|
|
setState(() {
|
|
loading = false;
|
|
});
|
|
print('Aucun produit trouvé.');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.black,
|
|
automaticallyImplyLeading: false,
|
|
leading: IconButton(
|
|
icon: Icon(Icons.arrow_back_ios, color: Colors.white, size: 24.0),
|
|
onPressed: () async {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
title: Text('Scanner'),
|
|
actions: [
|
|
Padding(
|
|
padding: EdgeInsetsDirectional.fromSTEB(8.0, 0.0, 8.0, 0.0),
|
|
child: IconButton(
|
|
icon: Icon(Icons.help_outline, color: Colors.white, size: 24.0),
|
|
onPressed: () {
|
|
print('IconButton pressed ...');
|
|
},
|
|
),
|
|
),
|
|
],
|
|
centerTitle: true,
|
|
elevation: 0.0,
|
|
),
|
|
body: loading
|
|
? Center(child: CircularProgressIndicator())
|
|
: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
child: Stack(
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
child: MobileScanner(
|
|
controller: mobileScannerController,
|
|
onDetectError: (error, stackTrace) {
|
|
print("===========> $error");
|
|
},
|
|
errorBuilder: (c, p2) {
|
|
return Center(child: Icon(Icons.error));
|
|
},
|
|
),
|
|
),
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Color(0xB7000000),
|
|
Colors.transparent,
|
|
Color(0xB7000000),
|
|
],
|
|
stops: [0.0, 0.5, 1.0],
|
|
begin: AlignmentDirectional(0.0, -1.0),
|
|
end: AlignmentDirectional(0, 1.0),
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsetsDirectional.fromSTEB(
|
|
24.0,
|
|
40.0,
|
|
24.0,
|
|
0.0,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'Scanner le code',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsetsDirectional.fromSTEB(
|
|
0.0,
|
|
8.0,
|
|
0.0,
|
|
0.0,
|
|
),
|
|
child: Text(
|
|
'Positionnez le code-barres ou QR code dans le cadre',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Align(
|
|
alignment: AlignmentDirectional(0.0, 0.0),
|
|
child: Container(
|
|
width: 280.0,
|
|
height: 280.0,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16.0),
|
|
border: Border.all(
|
|
color: Colors.white,
|
|
width: 3.0,
|
|
),
|
|
),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
_fakeBarcode();
|
|
qrcodeFound = false;
|
|
mobileScannerController.start();
|
|
},
|
|
child: Container(
|
|
width: 260.0,
|
|
height: 260.0,
|
|
decoration: BoxDecoration(
|
|
color: Color(0x33FFFFFF),
|
|
borderRadius: BorderRadius.circular(12.0),
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.qr_code_scanner,
|
|
color: Colors.white,
|
|
size: 64.0,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsetsDirectional.fromSTEB(
|
|
24.0,
|
|
0.0,
|
|
24.0,
|
|
40.0,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
spacing: 12,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'Types de codes supportés:',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Container(
|
|
width: 80.0,
|
|
height: 40.0,
|
|
decoration: BoxDecoration(
|
|
color: Color(0x33FFFFFF),
|
|
borderRadius: BorderRadius.circular(
|
|
8.0,
|
|
),
|
|
),
|
|
child: Align(
|
|
alignment: AlignmentDirectional(
|
|
0.0,
|
|
0.0,
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'EAN-13',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
width: 80.0,
|
|
height: 40.0,
|
|
decoration: BoxDecoration(
|
|
color: Color(0x33FFFFFF),
|
|
borderRadius: BorderRadius.circular(
|
|
8.0,
|
|
),
|
|
),
|
|
child: Align(
|
|
alignment: AlignmentDirectional(
|
|
0.0,
|
|
0.0,
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'Code 128',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
width: 80.0,
|
|
height: 40.0,
|
|
decoration: BoxDecoration(
|
|
color: Color(0x33FFFFFF),
|
|
borderRadius: BorderRadius.circular(
|
|
8.0,
|
|
),
|
|
),
|
|
child: Align(
|
|
alignment: AlignmentDirectional(
|
|
0.0,
|
|
0.0,
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'QR Code',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
IconButton(
|
|
color: Color(0x33FFFFFF),
|
|
icon: Icon(
|
|
mobileScannerController.torchEnabled
|
|
? Icons.flash_off
|
|
: Icons.flash_on,
|
|
color: Colors.white,
|
|
size: 28.0,
|
|
),
|
|
onPressed: () {
|
|
mobileScannerController.toggleTorch();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|