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