mandreshope 94b4fc1723 refactor: Refactor login flow and add product form route
Refactors the login page and model for improved separation of concerns. Moves navigation logic from the model to the page using success callbacks. Integrates a dedicated primary button component for the login action, showing loading state.

Adds a new secure route for a product form page and updates the scanner page to navigate to it when viewing scanned product details.

Simplifies the callback signature for viewing product details in the scanned product component. Also includes minor adjustments to the splash screen delay and theme definition.
2025-06-23 10:22:24 +03:00

500 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:barcode_scanner/router/go_secure_router_builder.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: 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: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute().push(context);
},
),
),
),
);
},
);
} 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: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute().push(context);
},
),
),
),
);
},
);
} 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();
},
),
],
),
],
),
),
],
),
),
),
],
),
),
);
}
}