mirror of
				https://github.com/StefBuwalda/cal_counter.git
				synced 2025-10-30 11:19:59 +00:00 
			
		
		
		
	Add barcode scanning and nutrition lookup feature
Introduces a new /scan route and template for barcode scanning using ZXing in the browser. Adds a /nutri/<barcode> API endpoint to fetch food item nutrition data by barcode. Updates the FoodItem model to include a barcode field and a to_dict method for JSON serialization. Also updates seed data to include a barcode.
This commit is contained in:
		
							
								
								
									
										25
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								app.py
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| from flask import render_template, redirect, url_for | ||||
| from flask import render_template, redirect, url_for, request, jsonify | ||||
| from flask_login import ( | ||||
|     login_required, | ||||
|     logout_user, | ||||
| @@ -6,7 +6,7 @@ from flask_login import ( | ||||
|     current_user, | ||||
| ) | ||||
| from forms import LoginForm | ||||
| from models import User | ||||
| from models import User, FoodItem | ||||
| from application import db, app, login_manager | ||||
| from application.admin.routes import admin_bp | ||||
|  | ||||
| @@ -43,8 +43,9 @@ def login(): | ||||
|         user = User.query.filter_by(username=form.username.data).first() | ||||
|         if user and user.check_password(password=form.password.data): | ||||
|             # User found and password correct | ||||
|             login_user(user) | ||||
|             return redirect(url_for("dashboard")) | ||||
|             next_page = request.args.get("next")  # Get next page if given | ||||
|             login_user(user)  # Log in the user | ||||
|             return redirect(next_page or url_for("dashboard")) | ||||
|         else: | ||||
|             pass | ||||
|             # invalid user | ||||
| @@ -64,6 +65,22 @@ def logout(): | ||||
|     return redirect(url_for("index")) | ||||
|  | ||||
|  | ||||
| @app.route("/scan") | ||||
| @login_required | ||||
| def scan(): | ||||
|     return render_template("scan.html") | ||||
|  | ||||
|  | ||||
| @app.route("/nutri/<int:barcode>", methods=["GET"]) | ||||
| @login_required | ||||
| def nutri(barcode): | ||||
|     food = FoodItem.query.filter_by(barcode=barcode).first() | ||||
|     if food: | ||||
|         return jsonify(food.to_dict()) | ||||
|     else: | ||||
|         return jsonify({}) | ||||
|  | ||||
|  | ||||
| # Run | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|   | ||||
							
								
								
									
										108
									
								
								application/templates/scan.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								application/templates/scan.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block content %} | ||||
| <div id="main" class="container text-center"> | ||||
| </div> | ||||
|  | ||||
| <template id="scan_result"> | ||||
|     <div> | ||||
|         <h5>Result:</h5> | ||||
|         <p id="result" class="fw-bold text-success"></p> | ||||
|     </div> | ||||
|  | ||||
|     <div id="product-info"></div> | ||||
| </template> | ||||
|  | ||||
| <template id="template_Reader"> | ||||
|     <h1 class="mb-4">📷 ZXing Barcode Scanner</h1> | ||||
|  | ||||
|     <div class="mb-3"> | ||||
|         <video id="video" class="border rounded shadow-sm" width="100%" style="max-width: 500px;"></video> | ||||
|     </div> | ||||
|  | ||||
|     <div class="mb-3"> | ||||
|         <button id="startButton" class="btn btn-primary">Start Scanning</button> | ||||
|         <button id="stopButton" class="btn btn-danger ms-2">Stop</button> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script type="module"> | ||||
|     import { BrowserMultiFormatReader } from 'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/+esm'; | ||||
|  | ||||
|     const mainElement = document.getElementById('main'); | ||||
|     const readerTemplate = document.getElementById('template_Reader') | ||||
|     const readerClone = readerTemplate.content.cloneNode(true); | ||||
|     const resultTemplate = document.getElementById('scan_result') | ||||
|     const resultClone = resultTemplate.content.cloneNode(true); | ||||
|     mainElement.appendChild(readerClone); | ||||
|  | ||||
|     // constants | ||||
|     const codeReader = new BrowserMultiFormatReader(); | ||||
|     const videoElement = document.getElementById('video'); | ||||
|  | ||||
|  | ||||
|     async function fetchProductData(barcode) { | ||||
|         // Step 1: GET Data | ||||
|         const response = await fetch(`/nutri/${barcode}`); | ||||
|         // Step 2: Check if response wasn't ok | ||||
|         if (!response.ok) throw new Error('Network response was not ok'); | ||||
|         // Step 3: Convert response to json and check if empty | ||||
|         const nutritionData = await response.json(); | ||||
|         mainElement.innerHTML = ''; | ||||
|         mainElement.appendChild(resultClone); | ||||
|         const resultElement = document.getElementById('result'); | ||||
|         resultElement.textContent = barcode; | ||||
|         const container = document.getElementById('product-info'); | ||||
|         if (Object.keys(nutritionData).length === 0) { | ||||
|             // No data, enter new data | ||||
|         } | ||||
|         else { | ||||
|             container.innerHTML = ` | ||||
|                     <h2>${nutritionData.name}</h2> | ||||
|                     <p><strong>Barcode:</strong> ${nutritionData.barcode}</p> | ||||
|                     <p><strong>Energy:</strong> ${nutritionData.energy_100g} kcal per 100g</p> | ||||
|                     <p><strong>Carbs:</strong> ${nutritionData.carbs_100g} g</p> | ||||
|                     <p><strong>Sugar:</strong> ${nutritionData.sugar_100g} g</p> | ||||
|                     <p><strong>Fats:</strong> ${nutritionData.fats_100g} g</p> | ||||
|                     <p><strong>Saturated Fats:</strong> ${nutritionData.saturated_fats_100g} g</p> | ||||
|                     <p><strong>Protein:</strong> ${nutritionData.protein_100g} g</p> | ||||
|                     `; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // Start scanning for barcode | ||||
|     document.getElementById('startButton').addEventListener('click', async () => { | ||||
|         console.log('[DEBUG] Start button clicked') | ||||
|         try { | ||||
|             await navigator.mediaDevices.getUserMedia({ video: true }); | ||||
|             // Use stream with video.srcObject = stream | ||||
|         } catch (err) { | ||||
|             alert("No camera found or no camera permission"); | ||||
|             console.error("Could not access the camera:", err); | ||||
|             return; | ||||
|         } | ||||
|         console.log('[DEBUG] Permission given and at least one device present'); | ||||
|         const devices = await codeReader.listVideoInputDevices(); | ||||
|         console.log('[DEBUG] Cameras found:', devices); | ||||
|         const selectedDeviceId = devices[0]?.deviceId; | ||||
|         await codeReader.decodeFromVideoDevice(selectedDeviceId, videoElement, async (result, err) => { | ||||
|             if (result) { | ||||
|                 const codeText = result.getText(); | ||||
|                 try { | ||||
|                     await fetchProductData(codeText) | ||||
|                     codeReader.reset(); | ||||
|                 } catch (error) { | ||||
|                     console.error('Error fetching nutrition data:', error); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     document.getElementById('stopButton').addEventListener('click', () => { | ||||
|         codeReader.reset(); | ||||
|         resultElement.textContent = ''; | ||||
|         container.innerHTML = "" | ||||
|     }); | ||||
| </script> | ||||
| {% endblock %} | ||||
							
								
								
									
										20
									
								
								models.py
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								models.py
									
									
									
									
									
								
							| @@ -36,6 +36,7 @@ class Unit(db.Model): | ||||
| class FoodItem(db.Model): | ||||
|     __tablename__ = "food_item" | ||||
|     id = db.Column(db.Integer, primary_key=True) | ||||
|     barcode = db.Column(db.Integer) | ||||
|     name = db.Column(db.String(150), unique=True, nullable=False) | ||||
|  | ||||
|     energy_100g = db.Column(db.Integer, nullable=False) | ||||
| @@ -52,8 +53,9 @@ class FoodItem(db.Model): | ||||
|         protein: float, | ||||
|         carbs: int, | ||||
|         fats: int, | ||||
|         sugar: Optional[int] = False, | ||||
|         saturated_fats: Optional[int] = False, | ||||
|         sugar: Optional[int] = None, | ||||
|         saturated_fats: Optional[int] = None, | ||||
|         barcode: Optional[int] = None, | ||||
|     ): | ||||
|         self.name = name | ||||
|         self.energy_100g = energy | ||||
| @@ -62,3 +64,17 @@ class FoodItem(db.Model): | ||||
|         self.sugar_100g = sugar | ||||
|         self.fats_100g = fats | ||||
|         self.saturated_fats_100g = saturated_fats | ||||
|         self.barcode = barcode | ||||
|  | ||||
|     def to_dict(self): | ||||
|         return { | ||||
|             "id": self.id, | ||||
|             "barcode": self.barcode, | ||||
|             "name": self.name, | ||||
|             "energy_100g": self.energy_100g, | ||||
|             "protein_100g": self.protein_100g, | ||||
|             "carbs_100g": self.carbs_100g, | ||||
|             "sugar_100g": self.sugar_100g, | ||||
|             "fats_100g": self.fats_100g, | ||||
|             "saturated_fats_100g": self.saturated_fats_100g, | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user