mandreshope 44289e29dc feat: Adds product image field to data models
Includes an `image` field in the `ProductStruct` and `ProductEntity` data models.

Updates the ObjectBox schema and regenerates the necessary code to persist the new field.

Refactors the `ProductScannedComponent` to accept a `ProductStruct` object, simplifying parameter passing.

Updates the `ScannerPage` to build and pass the `ProductStruct` to the component, enabling the display of the product image and using the structured data for other details like quantity. The 'Marque' display is replaced with 'Quantité' in the component.
2025-07-03 17:20:44 +03:00

421 lines
16 KiB
Dart

import 'dart:async';
import 'package:barcode_scanner/backend/api/api_calls.dart';
import 'package:barcode_scanner/backend/schema/product/product_struct.dart';
import 'package:barcode_scanner/components/product_scanned_component.dart';
import 'package:barcode_scanner/router/go_secure_router_builder.dart';
import 'package:barcode_scanner/themes/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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;
});
debugPrint('Nom du produit : ${product["product_name"]}');
debugPrint('Marque : ${product["brands"]}');
debugPrint('Image : ${product["image_url"]}');
final productStruct = ProductStruct(
id: int.parse(product["id"]),
name: product["product_name"],
image: product["image_thumb_url"],
description: product["categories"],
quantity: product["quantity"],
);
//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(
productStruct: productStruct,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute().push(context);
},
),
),
),
);
},
);
} else {
setState(() {
loading = false;
});
debugPrint('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;
});
debugPrint('Nom du produit : ${product["product_name"]}');
debugPrint('Marque : ${product["brands"]}');
debugPrint('Image : ${product["image_url"]}');
final productStruct = ProductStruct(
id: int.parse(product["id"]),
image: product["image_thumb_url"],
name: product["product_name"],
description: product["categories"],
quantity: product["quantity"],
);
//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(
productStruct: productStruct,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute().push(context);
},
),
),
),
);
},
);
} else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppTheme.of(context).primaryBackground,
automaticallyImplyLeading: false,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: AppTheme.of(context).primaryText,
size: 24.0,
),
onPressed: () async {
Navigator.of(context).pop();
},
),
title: Text('Scanner', style: AppTheme.of(context).titleLarge),
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: () {
debugPrint('IconButton pressed ...');
},
),
),
],
centerTitle: true,
elevation: 0.0,
),
body: loading
? Center(child: CircularProgressIndicator())
: SizedBox(
width: double.infinity,
height: double.infinity,
child: Stack(
children: [
SizedBox(
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: [
AppTheme.of(context).primaryBackground,
Colors.transparent,
AppTheme.of(context).primaryBackground,
],
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: AppTheme.of(context).primaryText,
),
),
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: AppTheme.of(context).primaryText,
),
),
),
],
),
),
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.withValues(
alpha: .5,
),
size: 64.0,
),
],
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(40),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
color: AppTheme.of(context).primaryText,
iconSize: 2,
icon: Icon(
mobileScannerController.torchEnabled
? Icons.flash_off
: Icons.flash_on,
color: AppTheme.of(context).primaryText,
size: 28.0,
),
onPressed: () {
mobileScannerController.toggleTorch();
},
),
],
),
),
],
),
),
),
],
),
),
);
}
}