find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Sql)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Sql)
-set(PROJECT_SOURCES
- src/main.cpp
- src/mainwindow.cpp
- include/mainwindow.h
- src/db/databasemanager.cpp
- include/db/databasemanager.h
- src/db/foodrepository.cpp
- include/db/foodrepository.h
- src/widgets/searchwidget.cpp
- include/widgets/searchwidget.h
- src/widgets/detailswidget.cpp
- include/widgets/detailswidget.h
- src/widgets/mealwidget.cpp
- include/widgets/mealwidget.h
- src/utils/string_utils.cpp
- include/utils/string_utils.h
- resources.qrc
+file(GLOB_RECURSE PROJECT_SOURCES
+ "src/*.cpp"
+ "include/*.h"
+ "resources.qrc"
)
# Versioning
bool connect(const QString &path);
[[nodiscard]] bool isOpen() const;
[[nodiscard]] QSqlDatabase database() const;
+ [[nodiscard]] QSqlDatabase userDatabase() const;
DatabaseManager(const DatabaseManager &) = delete;
DatabaseManager &operator=(const DatabaseManager &) = delete;
DatabaseManager();
~DatabaseManager();
+ void initUserDatabase();
+
QSqlDatabase m_db;
+ QSqlDatabase m_userDb;
};
#endif // DATABASEMANAGER_H
double rdaPercentage; // Calculated
};
+struct ServingWeight {
+ QString description;
+ double grams;
+};
+
struct FoodItem {
int id;
QString description;
- int foodGroupId;
+ QString foodGroupName;
int nutrientCount;
int aminoCount;
int flavCount;
// Returns a list of nutrients
std::vector<Nutrient> getFoodNutrients(int foodId);
+ // Get available serving weights (units) for a food
+ std::vector<ServingWeight> getFoodServings(int foodId);
+
+ // RDA methods
+ std::map<int, double> getNutrientRdas();
+ void updateRda(int nutrId, double value);
+
// Helper to get nutrient definition basics if needed
// QString getNutrientName(int nutrientId);
private:
// Internal helper methods
void ensureCacheLoaded();
+ void loadRdas();
bool m_cacheLoaded = false;
// Cache stores basic food info
std::vector<FoodItem> m_cache;
+ std::map<int, double> m_rdas;
};
#endif // FOODREPOSITORY_H
SearchWidget *searchWidget;
DetailsWidget *detailsWidget;
MealWidget *mealWidget;
+ FoodRepository repository;
QMenu *recentFilesMenu;
static constexpr int MaxRecentFiles = 5;
--- /dev/null
+#ifndef RDASETTINGSWIDGET_H
+#define RDASETTINGSWIDGET_H
+
+#include "db/foodrepository.h"
+#include <QDialog>
+#include <QTableWidget>
+
+class RDASettingsWidget : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit RDASettingsWidget(FoodRepository &repository,
+ QWidget *parent = nullptr);
+
+private slots:
+ void onCellChanged(int row, int column);
+
+private:
+ void loadData();
+
+ FoodRepository &m_repository;
+ QTableWidget *m_table;
+ bool m_loading = false;
+};
+
+#endif // RDASETTINGSWIDGET_H
--- /dev/null
+#ifndef WEIGHTINPUTDIALOG_H
+#define WEIGHTINPUTDIALOG_H
+
+#include "db/foodrepository.h"
+#include <QComboBox>
+#include <QDialog>
+#include <QDoubleSpinBox>
+#include <QLabel>
+
+class WeightInputDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit WeightInputDialog(const QString &foodName,
+ const std::vector<ServingWeight> &servings,
+ QWidget *parent = nullptr);
+
+ double getGrams() const;
+
+private:
+ QDoubleSpinBox *amountSpinBox;
+ QComboBox *unitComboBox;
+ std::vector<ServingWeight> m_servings;
+
+ static constexpr double GRAMS_PER_OZ = 28.3495;
+ static constexpr double GRAMS_PER_LB = 453.592;
+};
+
+#endif // WEIGHTINPUTDIALOG_H
#include "db/databasemanager.h"
#include <QDebug>
+#include <QDir>
#include <QFileInfo>
#include <QSqlError>
+#include <QVariant>
DatabaseManager &DatabaseManager::instance() {
static DatabaseManager instance;
return instance;
}
-DatabaseManager::DatabaseManager() = default;
+DatabaseManager::DatabaseManager() {
+ m_userDb = QSqlDatabase::addDatabase("QSQLITE", "user_db");
+ initUserDatabase();
+}
DatabaseManager::~DatabaseManager() {
if (m_db.isOpen()) {
bool DatabaseManager::isOpen() const { return m_db.isOpen(); }
QSqlDatabase DatabaseManager::database() const { return m_db; }
+
+QSqlDatabase DatabaseManager::userDatabase() const { return m_userDb; }
+
+void DatabaseManager::initUserDatabase() {
+ QString path = QDir::homePath() + "/.nutra/nt.sqlite3";
+ m_userDb.setDatabaseName(path);
+
+ if (!m_userDb.open()) {
+ qCritical() << "Failed to open user database:"
+ << m_userDb.lastError().text();
+ return;
+ }
+
+ QSqlQuery query(m_userDb);
+ // Create profile table (simplified version of CLI's schema)
+ if (!query.exec("CREATE TABLE IF NOT EXISTS profile ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT, "
+ "name TEXT UNIQUE NOT NULL)")) {
+ qCritical() << "Failed to create profile table:"
+ << query.lastError().text();
+ }
+
+ // Ensure default profile exists
+ query.exec("INSERT OR IGNORE INTO profile (id, name) VALUES (1, 'default')");
+
+ // Create rda table
+ if (!query.exec("CREATE TABLE IF NOT EXISTS rda ("
+ "profile_id INTEGER NOT NULL, "
+ "nutr_id INTEGER NOT NULL, "
+ "rda REAL NOT NULL, "
+ "PRIMARY KEY (profile_id, nutr_id), "
+ "FOREIGN KEY (profile_id) REFERENCES profile (id))")) {
+ qCritical() << "Failed to create rda table:" << query.lastError().text();
+ }
+}
if (!db.isOpen())
return;
- // 1. Load Food Items
- QSqlQuery query("SELECT id, long_desc, fdgrp_id FROM food_des", db);
+ // 1. Load Food Items with Group Names
+ QSqlQuery query("SELECT f.id, f.long_desc, g.fdgrp_desc "
+ "FROM food_des f "
+ "JOIN fdgrp g ON f.fdgrp_id = g.id",
+ db);
std::map<int, int> nutrientCounts;
// 2. Load Nutrient Counts (Bulk)
FoodItem item;
item.id = query.value(0).toInt();
item.description = query.value(1).toString();
- item.foodGroupId = query.value(2).toInt();
+ item.foodGroupName = query.value(2).toString();
// Set counts from map (default 0 if not found)
auto it = nutrientCounts.find(item.id);
item.score = 0;
m_cache.push_back(item);
}
+ loadRdas();
m_cacheLoaded = true;
}
+void FoodRepository::loadRdas() {
+ m_rdas.clear();
+ QSqlDatabase db = DatabaseManager::instance().database();
+ QSqlDatabase userDb = DatabaseManager::instance().userDatabase();
+
+ // 1. Load Defaults from USDA
+ if (db.isOpen()) {
+ QSqlQuery query("SELECT id, rda FROM nutrients_overview", db);
+ while (query.next()) {
+ m_rdas[query.value(0).toInt()] = query.value(1).toDouble();
+ }
+ }
+
+ // 2. Load Overrides from User DB
+ if (userDb.isOpen()) {
+ QSqlQuery query("SELECT nutr_id, rda FROM rda WHERE profile_id = 1",
+ userDb);
+ while (query.next()) {
+ m_rdas[query.value(0).toInt()] = query.value(1).toDouble();
+ }
+ }
+}
+
std::vector<FoodItem> FoodRepository::searchFoods(const QString &query) {
ensureCacheLoaded();
std::vector<FoodItem> results;
nut.amount = nutQuery.value(2).toDouble();
nut.description = nutQuery.value(3).toString();
nut.unit = nutQuery.value(4).toString();
- nut.rdaPercentage = 0.0;
+
+ if (m_rdas.count(nut.id) != 0U && m_rdas[nut.id] > 0) {
+ nut.rdaPercentage = (nut.amount / m_rdas[nut.id]) * 100.0;
+ } else {
+ nut.rdaPercentage = 0.0;
+ }
if (idToIndex.count(fid) != 0U) {
results[idToIndex[fid]].nutrients.push_back(nut);
nut.amount = query.value(1).toDouble();
nut.description = query.value(2).toString();
nut.unit = query.value(3).toString();
- nut.rdaPercentage = 0.0;
+
+ if (m_rdas.count(nut.id) != 0U && m_rdas[nut.id] > 0) {
+ nut.rdaPercentage = (nut.amount / m_rdas[nut.id]) * 100.0;
+ } else {
+ nut.rdaPercentage = 0.0;
+ }
results.push_back(nut);
}
return results;
}
+
+std::vector<ServingWeight> FoodRepository::getFoodServings(int foodId) {
+ std::vector<ServingWeight> results;
+ QSqlDatabase db = DatabaseManager::instance().database();
+
+ if (!db.isOpen())
+ return results;
+
+ QSqlQuery query(db);
+ if (!query.prepare("SELECT d.msre_desc, s.grams "
+ "FROM serving s "
+ "JOIN serv_desc d ON s.msre_id = d.id "
+ "WHERE s.food_id = ?")) {
+ qCritical() << "Prepare servings failed:" << query.lastError().text();
+ return results;
+ }
+
+ query.bindValue(0, foodId);
+
+ if (query.exec()) {
+ while (query.next()) {
+ ServingWeight sw;
+ sw.description = query.value(0).toString();
+ sw.grams = query.value(1).toDouble();
+ results.push_back(sw);
+ }
+ } else {
+ qCritical() << "Servings query failed:" << query.lastError().text();
+ }
+
+ return results;
+}
+
+std::map<int, double> FoodRepository::getNutrientRdas() {
+ ensureCacheLoaded();
+ return m_rdas;
+}
+
+void FoodRepository::updateRda(int nutrId, double value) {
+ QSqlDatabase userDb = DatabaseManager::instance().userDatabase();
+ if (!userDb.isOpen())
+ return;
+
+ QSqlQuery query(userDb);
+ query.prepare("INSERT OR REPLACE INTO rda (profile_id, nutr_id, rda) "
+ "VALUES (1, ?, ?)");
+ query.bindValue(0, nutrId);
+ query.bindValue(1, value);
+
+ if (query.exec()) {
+ m_rdas[nutrId] = value;
+ } else {
+ qCritical() << "Failed to update RDA:" << query.lastError().text();
+ }
+}
#include "mainwindow.h"
#include "db/databasemanager.h"
+#include "widgets/rdasettingswidget.h"
#include <QAction>
#include <QDebug>
#include <QFileDialog>
recentFilesMenu->addAction(recentFileActions[i]);
// Edit Menu
- auto *editMenu = menuBar()->addMenu("&Edit");
- auto *settingsAction = editMenu->addAction("&Settings");
+ QMenu *editMenu = menuBar()->addMenu("Edit");
+ QAction *rdaAction = editMenu->addAction("RDA Settings");
+ connect(rdaAction, &QAction::triggered, this, [this]() {
+ RDASettingsWidget dlg(repository, this);
+ dlg.exec();
+ });
+
+ QAction *settingsAction = editMenu->addAction("Settings");
connect(settingsAction, &QAction::triggered, this, &MainWindow::onSettings);
// Help Menu
--- /dev/null
+#include "widgets/rdasettingswidget.h"
+#include "db/databasemanager.h"
+#include <QHeaderView>
+#include <QLabel>
+#include <QSqlQuery>
+#include <QVBoxLayout>
+
+RDASettingsWidget::RDASettingsWidget(FoodRepository &repository,
+ QWidget *parent)
+ : QDialog(parent), m_repository(repository) {
+ setWindowTitle("RDA Settings");
+ resize(600, 400);
+
+ auto *layout = new QVBoxLayout(this);
+ layout->addWidget(new QLabel("Customize your Recommended Daily Allowances "
+ "(RDA). Changes are saved automatically."));
+
+ m_table = new QTableWidget(this);
+ m_table->setColumnCount(4);
+ m_table->setHorizontalHeaderLabels({"ID", "Nutrient", "RDA", "Unit"});
+ m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+ m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+
+ loadData();
+
+ connect(m_table, &QTableWidget::cellChanged, this,
+ &RDASettingsWidget::onCellChanged);
+
+ layout->addWidget(m_table);
+}
+
+void RDASettingsWidget::loadData() {
+ m_loading = true;
+ m_table->setRowCount(0);
+
+ QSqlDatabase db = DatabaseManager::instance().database();
+ if (!db.isOpen())
+ return;
+
+ // Get metadata from USDA
+ QSqlQuery query(
+ "SELECT id, nutr_desc, unit FROM nutrients_overview ORDER BY nutr_desc",
+ db);
+ auto currentRdas = m_repository.getNutrientRdas();
+
+ int row = 0;
+ while (query.next()) {
+ int id = query.value(0).toInt();
+ QString name = query.value(1).toString();
+ QString unit = query.value(2).toString();
+ double rda = currentRdas.count(id) != 0U ? currentRdas[id] : 0.0;
+
+ m_table->insertRow(row);
+
+ auto *idItem = new QTableWidgetItem(QString::number(id));
+ idItem->setFlags(idItem->flags() & ~Qt::ItemIsEditable);
+ m_table->setItem(row, 0, idItem);
+
+ auto *nameItem = new QTableWidgetItem(name);
+ nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable);
+ m_table->setItem(row, 1, nameItem);
+
+ auto *rdaItem = new QTableWidgetItem(QString::number(rda));
+ m_table->setItem(row, 2, rdaItem);
+
+ auto *unitItem = new QTableWidgetItem(unit);
+ unitItem->setFlags(unitItem->flags() & ~Qt::ItemIsEditable);
+ m_table->setItem(row, 3, unitItem);
+
+ row++;
+ }
+
+ m_loading = false;
+}
+
+void RDASettingsWidget::onCellChanged(int row, int column) {
+ if (m_loading || column != 2)
+ return;
+
+ int id = m_table->item(row, 0)->text().toInt();
+ double value = m_table->item(row, 2)->text().toDouble();
+
+ m_repository.updateRda(id, value);
+}
#include "widgets/searchwidget.h"
+#include "widgets/weightinputdialog.h"
#include <QAction>
#include <QHBoxLayout>
#include <QHeaderView>
-#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QVBoxLayout>
const auto &item = results[i];
resultsTable->setItem(i, 0, new QTableWidgetItem(QString::number(item.id)));
resultsTable->setItem(i, 1, new QTableWidgetItem(item.description));
- resultsTable->setItem(
- i, 2, new QTableWidgetItem(QString::number(item.foodGroupId)));
+ resultsTable->setItem(i, 2, new QTableWidgetItem(item.foodGroupName));
resultsTable->setItem(
i, 3, new QTableWidgetItem(QString::number(item.nutrientCount)));
resultsTable->setItem(
if (selectedAction == analyzeAction) {
emit foodSelected(foodId, foodName);
} else if (selectedAction == addToMealAction) {
- bool ok;
- double grams = QInputDialog::getDouble(
- this, "Add to Meal", "Amount (grams):", 100.0, 0.1, 10000.0, 1, &ok);
- if (ok) {
- emit addToMealRequested(foodId, foodName, grams);
+ std::vector<ServingWeight> servings = repository.getFoodServings(foodId);
+ WeightInputDialog dlg(foodName, servings, this);
+ if (dlg.exec() == QDialog::Accepted) {
+ emit addToMealRequested(foodId, foodName, dlg.getGrams());
}
}
}
--- /dev/null
+#include "widgets/weightinputdialog.h"
+#include <QHBoxLayout>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+WeightInputDialog::WeightInputDialog(const QString &foodName,
+ const std::vector<ServingWeight> &servings,
+ QWidget *parent)
+ : QDialog(parent), m_servings(servings) {
+ setWindowTitle("Add to Meal - " + foodName);
+ auto *layout = new QVBoxLayout(this);
+
+ layout->addWidget(
+ new QLabel("How much " + foodName + " are you adding?", this));
+
+ auto *inputLayout = new QHBoxLayout();
+ amountSpinBox = new QDoubleSpinBox(this);
+ amountSpinBox->setRange(0.1, 10000.0);
+ amountSpinBox->setValue(1.0);
+ amountSpinBox->setDecimals(2);
+
+ unitComboBox = new QComboBox(this);
+ unitComboBox->addItem("Grams (g)", 1.0);
+ unitComboBox->addItem("Ounces (oz)", GRAMS_PER_OZ);
+ unitComboBox->addItem("Pounds (lb)", GRAMS_PER_LB);
+
+ for (const auto &sw : servings) {
+ unitComboBox->addItem(sw.description, sw.grams);
+ }
+
+ // Default to Grams and set value to 100 if Grams is selected
+ unitComboBox->setCurrentIndex(0);
+ amountSpinBox->setValue(100.0);
+
+ // Update value when unit changes? No, let's keep it simple.
+ // Usually 100g is a good default, but 1 serving might be better if available.
+ if (!servings.empty()) {
+ unitComboBox->setCurrentIndex(3); // First serving
+ amountSpinBox->setValue(1.0);
+ }
+
+ inputLayout->addWidget(amountSpinBox);
+ inputLayout->addWidget(unitComboBox);
+ layout->addLayout(inputLayout);
+
+ auto *buttonLayout = new QHBoxLayout();
+ auto *okButton = new QPushButton("Add to Meal", this);
+ auto *cancelButton = new QPushButton("Cancel", this);
+
+ connect(okButton, &QPushButton::clicked, this, &QDialog::accept);
+ connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
+
+ buttonLayout->addStretch();
+ buttonLayout->addWidget(cancelButton);
+ buttonLayout->addWidget(okButton);
+ layout->addLayout(buttonLayout);
+}
+
+double WeightInputDialog::getGrams() const {
+ double amount = amountSpinBox->value();
+ double multiplier = unitComboBox->currentData().toDouble();
+ return amount * multiplier;
+}