From efe888a50913a5df59a98317c345b213c5eabecc Mon Sep 17 00:00:00 2001 From: Shane Jaroch Date: Tue, 20 Jan 2026 16:02:48 -0500 Subject: [PATCH] initial commit - ubuntu-20.04 (Qt 5.12) support - makefile - update title; import icon so it hopefully loads - install stuff (desktop file for linux launcher) - update app icon; add GitHub workflows - clang lint - fix lint error in tests (unregistered macro) --- .clang-tidy | 8 ++ .clangd | 3 + .github/workflows/arch.yml.disabled | 30 +++++ .github/workflows/ci-full.yml | 40 +++++++ .github/workflows/macos.yml | 35 ++++++ .github/workflows/ubuntu-20.04.yml | 45 +++++++ .github/workflows/ubuntu-22.04.yml | 39 ++++++ .github/workflows/ubuntu-24.04.yml | 39 ++++++ .github/workflows/windows.yml | 35 ++++++ .gitignore | 10 ++ .gitmodules | 3 + CMakeLists.txt | 64 ++++++++++ Makefile | 80 +++++++++++++ include/db/databasemanager.h | 25 ++++ include/db/foodrepository.h | 50 ++++++++ include/mainwindow.h | 26 ++++ include/utils/string_utils.h | 19 +++ include/widgets/detailswidget.h | 34 ++++++ include/widgets/mealwidget.h | 40 +++++++ include/widgets/searchwidget.h | 32 +++++ nutra.desktop | 10 ++ resources.qrc | 6 + resources/nutrition_icon-no_bg.png | Bin 0 -> 51265 bytes src/db/databasemanager.cpp | 43 +++++++ src/db/foodrepository.cpp | 177 ++++++++++++++++++++++++++++ src/main.cpp | 68 +++++++++++ src/mainwindow.cpp | 52 ++++++++ src/utils/string_utils.cpp | 112 ++++++++++++++++++ src/widgets/detailswidget.cpp | 63 ++++++++++ src/widgets/mealwidget.cpp | 98 +++++++++++++++ src/widgets/searchwidget.cpp | 85 +++++++++++++ tests/test_foodrepository.cpp | 58 +++++++++ usdasqlite | 1 + 33 files changed, 1430 insertions(+) create mode 100644 .clang-tidy create mode 100644 .clangd create mode 100644 .github/workflows/arch.yml.disabled create mode 100644 .github/workflows/ci-full.yml create mode 100644 .github/workflows/macos.yml create mode 100644 .github/workflows/ubuntu-20.04.yml create mode 100644 .github/workflows/ubuntu-22.04.yml create mode 100644 .github/workflows/ubuntu-24.04.yml create mode 100644 .github/workflows/windows.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 include/db/databasemanager.h create mode 100644 include/db/foodrepository.h create mode 100644 include/mainwindow.h create mode 100644 include/utils/string_utils.h create mode 100644 include/widgets/detailswidget.h create mode 100644 include/widgets/mealwidget.h create mode 100644 include/widgets/searchwidget.h create mode 100644 nutra.desktop create mode 100644 resources.qrc create mode 100644 resources/nutrition_icon-no_bg.png create mode 100644 src/db/databasemanager.cpp create mode 100644 src/db/foodrepository.cpp create mode 100644 src/main.cpp create mode 100644 src/mainwindow.cpp create mode 100644 src/utils/string_utils.cpp create mode 100644 src/widgets/detailswidget.cpp create mode 100644 src/widgets/mealwidget.cpp create mode 100644 src/widgets/searchwidget.cpp create mode 100644 tests/test_foodrepository.cpp create mode 160000 usdasqlite diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..9c64a3b --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,8 @@ +--- +Checks: '-*,readability-*,performance-*,bugprone-*,modernize-*,-modernize-use-trailing-return-type,-readability-identifier-length,-readability-magic-numbers,-readability-braces-around-statements,-bugprone-easily-swappable-parameters,-readability-convert-member-functions-to-static,-readability-redundant-access-specifiers,-readability-redundant-casting' +WarningsAsErrors: '' +HeaderFilterRegex: 'src/.*' +# Ignore specific Qt/moc artifacts if they leak in +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..dddeaef --- /dev/null +++ b/.clangd @@ -0,0 +1,3 @@ +CompileFlags: + Remove: + - -mno-direct-extern-access diff --git a/.github/workflows/arch.yml.disabled b/.github/workflows/arch.yml.disabled new file mode 100644 index 0000000..f8e5ea6 --- /dev/null +++ b/.github/workflows/arch.yml.disabled @@ -0,0 +1,30 @@ +name: Arch Linux + +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master] + +jobs: + build: + name: "Arch Linux" + runs-on: ubuntu-latest + container: + image: archlinux:latest + + steps: + - name: install dependencies + run: pacman -Syyu --noconfirm git cmake base-devel unbound boost qrencode qt6-base qt6-svg qt6-websockets qt6-wayland qt6-multimedia libzip hidapi protobuf zxing-cpp + + - name: configure git + run: git config --global --add safe.directory '*' + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: build + run: | + cmake -S . -B build + cmake --build build -j $(nproc) diff --git a/.github/workflows/ci-full.yml b/.github/workflows/ci-full.yml new file mode 100644 index 0000000..9d628df --- /dev/null +++ b/.github/workflows/ci-full.yml @@ -0,0 +1,40 @@ +name: Full CI + +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master] + +jobs: + full-check: + name: Full CI Check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y git cmake build-essential ccache \ + qtbase5-dev libqt5sql5-sqlite libgl1-mesa-dev \ + clang-format cppcheck clang-tidy + + - name: Check Formatting + run: | + make format + git diff + git diff --stat + git diff --quiet --exit-code + + - name: Lint + run: make lint + + - name: Build Release + run: make release + + - name: Test + run: make test diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..b20da2d --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,35 @@ +name: macOS + +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master] + +jobs: + build: + name: Build + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: "6.5.0" + host: "mac" + + - name: Build Release + run: make release + + - name: Test + run: make test + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: nutra-macos + path: build/nutra.app diff --git a/.github/workflows/ubuntu-20.04.yml b/.github/workflows/ubuntu-20.04.yml new file mode 100644 index 0000000..c3f75f6 --- /dev/null +++ b/.github/workflows/ubuntu-20.04.yml @@ -0,0 +1,45 @@ +--- +name: Ubuntu 20.04 + +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master] + +jobs: + build: + name: "Ubuntu 20.04 (Qt 5.12)" + runs-on: ubuntu-24.04 + container: + image: ubuntu:20.04 + env: + DEBIAN_FRONTEND: noninteractive + + steps: + - name: update apt + run: apt update + + - name: install dependencies + run: | + apt -y install git cmake build-essential ccache \ + qtbase5-dev libqt5sql5-sqlite libgl1-mesa-dev + + - name: configure git + run: git config --global --add safe.directory '*' + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build Release + run: make release + + - name: Test + run: make test + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: nutra-ubuntu-20.04 + path: build/nutra diff --git a/.github/workflows/ubuntu-22.04.yml b/.github/workflows/ubuntu-22.04.yml new file mode 100644 index 0000000..0ec2dc6 --- /dev/null +++ b/.github/workflows/ubuntu-22.04.yml @@ -0,0 +1,39 @@ +--- +name: Ubuntu 22.04 + +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master] + +jobs: + build: + name: "Ubuntu 22.04 (Qt 5.15)" + runs-on: [self-hosted, ubuntu-22.04] + + steps: + # NOTE: Dependencies are already installed on the dev runner + # - name: update apt + # run: sudo apt-get update + + # - name: install dependencies + # run: | + # sudo apt-get install -y git cmake build-essential ccache \ + # qtbase5-dev libqt5sql5-sqlite libgl1-mesa-dev + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build Release + run: make release + + - name: Test + run: make test + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: nutra-ubuntu-22.04 + path: build/nutra diff --git a/.github/workflows/ubuntu-24.04.yml b/.github/workflows/ubuntu-24.04.yml new file mode 100644 index 0000000..0395eff --- /dev/null +++ b/.github/workflows/ubuntu-24.04.yml @@ -0,0 +1,39 @@ +--- +name: Ubuntu 24.04 + +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master] + +jobs: + build: + name: "Ubuntu 24.04 (Qt 6)" + runs-on: [self-hosted, ubuntu-24.04] + + steps: + # NOTE: Dependencies are already installed on the dev runner + # - name: update apt + # run: sudo apt-get update + + # - name: install dependencies + # run: | + # sudo apt-get install -y git cmake build-essential ccache \ + # qtbase5-dev libqt5sql5-sqlite libgl1-mesa-dev + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build Release + run: make release + + - name: Test + run: make test + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: nutra-ubuntu-24.04 + path: build/nutra diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..0139cb1 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,35 @@ +name: Windows + +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master] + +jobs: + build: + name: Build + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: "6.5.0" + host: "windows" + + - name: Build + run: make release + + - name: Test + run: make test + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: nutra-win64 + path: build/Release/nutra.exe diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25244e --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# macOS/backup files +.~* +._* +~$* +.DS_Store +*.swp + +# C++ stuff +build/ + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ae4efcd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "usdasqlite"] + path = usdasqlite + url = https://github.com/nutratech/usda-sqlite.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..aaec69d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.16) + +project(nutra LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# Find Qt6 or Qt5 +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 +) + + + + + + +add_executable(nutra + ${PROJECT_SOURCES} +) + +target_include_directories(nutra PRIVATE ${CMAKE_SOURCE_DIR}/include) + +target_link_libraries(nutra PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Sql) + +enable_testing() +find_package(Qt${QT_VERSION_MAJOR}Test REQUIRED) + +add_executable(test_nutra EXCLUDE_FROM_ALL tests/test_foodrepository.cpp src/db/databasemanager.cpp src/db/foodrepository.cpp src/utils/string_utils.cpp) +target_include_directories(test_nutra PRIVATE ${CMAKE_SOURCE_DIR}/include) +target_link_libraries(test_nutra PRIVATE Qt${QT_VERSION_MAJOR}::Test Qt${QT_VERSION_MAJOR}::Sql) + +add_test(NAME FoodRepoTest COMMAND test_nutra) + + +install(TARGETS nutra DESTINATION bin) +install(FILES nutra.desktop DESTINATION share/applications) +install(FILES resources/nutrition_icon-no_bg.png DESTINATION share/icons/hicolor/128x128/apps RENAME nutra.png) + +if(NUTRA_DB_FILE AND EXISTS "${NUTRA_DB_FILE}") + install(FILES "${NUTRA_DB_FILE}" DESTINATION share/nutra RENAME usda.sqlite3) +endif() diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d10b8fc --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +SHELL := /bin/bash +BUILD_DIR := build +CMAKE := cmake +CTEST := ctest +SRC_DIRS := src + +# Find source files for linting +LINT_LOCS_CPP ?= $(shell git ls-files '*.cpp') +LINT_LOCS_H ?= $(shell git ls-files '*.h') + +.PHONY: config +config: + $(CMAKE) -E make_directory $(BUILD_DIR) + $(CMAKE) \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -B $(BUILD_DIR) + +.PHONY: debug +debug: config + $(CMAKE) --build $(BUILD_DIR) --config Debug + +.PHONY: release +release: + $(CMAKE) -E make_directory $(BUILD_DIR) + $(CMAKE) -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release + $(CMAKE) --build $(BUILD_DIR) --config Release + +.PHONY: clean +clean: + $(CMAKE) -E remove_directory $(BUILD_DIR) + + +.PHONY: test +test: release + $(CMAKE) --build $(BUILD_DIR) --target test_nutra --config Release + cd $(BUILD_DIR) && $(CTEST) --output-on-failure -C Release + +.PHONY: run +run: debug + ./$(BUILD_DIR)/nutra + + +.PHONY: format +format: + -prettier --write .github/ + clang-format -i $(LINT_LOCS_CPP) $(LINT_LOCS_H) + + +.PHONY: lint +lint: config + @echo "Linting..." + @# Build test target first to generate MOC files for tests + @$(CMAKE) --build $(BUILD_DIR) --target test_nutra --config Debug 2>/dev/null || true + @echo "Running cppcheck..." + cppcheck --enable=warning,performance,portability \ + --language=c++ --std=c++17 \ + --suppress=missingIncludeSystem \ + -Dslots= -Dsignals= -Demit= \ + --quiet --error-exitcode=1 \ + $(SRC_DIRS) include tests + @if [ ! -f $(BUILD_DIR)/compile_commands.json ]; then \ + echo "Error: compile_commands.json not found in $(BUILD_DIR). Run 'make config' first."; \ + exit 1; \ + fi + @# Create a temp clean compile_commands.json to avoid modifying the original + @mkdir -p $(BUILD_DIR)/lint_tmp + @sed 's/-mno-direct-extern-access//g' $(BUILD_DIR)/compile_commands.json > $(BUILD_DIR)/lint_tmp/compile_commands.json + @echo "Running clang-tidy in parallel..." + @echo $(LINT_LOCS_CPP) $(LINT_LOCS_H) | xargs -n 1 -P $(shell nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 1) clang-tidy --quiet -p $(BUILD_DIR)/lint_tmp -extra-arg=-Wno-unknown-argument + @rm -rf $(BUILD_DIR)/lint_tmp + + +.PHONY: install +install: release + @echo "Installing..." + @$(MAKE) -C $(BUILD_DIR) install || ( \ + $(CMAKE) -DCMAKE_INSTALL_PREFIX=$(HOME)/.local -B $(BUILD_DIR) && \ + $(MAKE) -C $(BUILD_DIR) install \ + ) diff --git a/include/db/databasemanager.h b/include/db/databasemanager.h new file mode 100644 index 0000000..32ad4ea --- /dev/null +++ b/include/db/databasemanager.h @@ -0,0 +1,25 @@ +#ifndef DATABASEMANAGER_H +#define DATABASEMANAGER_H + +#include +#include +#include + +class DatabaseManager { +public: + static DatabaseManager &instance(); + bool connect(const QString &path); + [[nodiscard]] bool isOpen() const; + [[nodiscard]] QSqlDatabase database() const; + + DatabaseManager(const DatabaseManager &) = delete; + DatabaseManager &operator=(const DatabaseManager &) = delete; + +private: + DatabaseManager(); + ~DatabaseManager(); + + QSqlDatabase m_db; +}; + +#endif // DATABASEMANAGER_H diff --git a/include/db/foodrepository.h b/include/db/foodrepository.h new file mode 100644 index 0000000..f90fc2b --- /dev/null +++ b/include/db/foodrepository.h @@ -0,0 +1,50 @@ +#ifndef FOODREPOSITORY_H +#define FOODREPOSITORY_H + +#include +#include +#include + +struct Nutrient { + int id; + QString description; + double amount; + QString unit; + double rdaPercentage; // Calculated +}; + +struct FoodItem { + int id; + QString description; + int foodGroupId; + int nutrientCount; + int aminoCount; + int flavCount; + int score; // For search results + std::vector nutrients; // Full details for results +}; + +class FoodRepository { +public: + explicit FoodRepository(); + + // Search foods by keyword + std::vector searchFoods(const QString &query); + + // Get detailed nutrients for a generic food (100g) + // Returns a list of nutrients + std::vector getFoodNutrients(int foodId); + + // Helper to get nutrient definition basics if needed + // QString getNutrientName(int nutrientId); + +private: + // Internal helper methods + void ensureCacheLoaded(); + + bool m_cacheLoaded = false; + // Cache stores basic food info + std::vector m_cache; +}; + +#endif // FOODREPOSITORY_H diff --git a/include/mainwindow.h b/include/mainwindow.h new file mode 100644 index 0000000..7253a88 --- /dev/null +++ b/include/mainwindow.h @@ -0,0 +1,26 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "widgets/detailswidget.h" +#include "widgets/mealwidget.h" +#include "widgets/searchwidget.h" +#include +#include + +class MainWindow : public QMainWindow { + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +private: + void setupUi(); + + QTabWidget *tabs; + SearchWidget *searchWidget; + DetailsWidget *detailsWidget; + MealWidget *mealWidget; +}; + +#endif // MAINWINDOW_H diff --git a/include/utils/string_utils.h b/include/utils/string_utils.h new file mode 100644 index 0000000..58c07b1 --- /dev/null +++ b/include/utils/string_utils.h @@ -0,0 +1,19 @@ +#ifndef STRING_UTILS_H +#define STRING_UTILS_H + +#include +#include +#include + +namespace Utils { + +// Calculate Levenshtein distance between two strings +int levenshteinDistance(const QString &s1, const QString &s2); + +// Calculate a simple fuzzy match score (0-100) +// Higher is better. +int calculateFuzzyScore(const QString &query, const QString &target); + +} // namespace Utils + +#endif // STRING_UTILS_H diff --git a/include/widgets/detailswidget.h b/include/widgets/detailswidget.h new file mode 100644 index 0000000..51b74e6 --- /dev/null +++ b/include/widgets/detailswidget.h @@ -0,0 +1,34 @@ +#ifndef DETAILSWIDGET_H +#define DETAILSWIDGET_H + +#include "db/foodrepository.h" +#include +#include +#include +#include + +class DetailsWidget : public QWidget { + Q_OBJECT + +public: + explicit DetailsWidget(QWidget *parent = nullptr); + + void loadFood(int foodId, const QString &foodName); + +signals: + void addToMeal(int foodId, const QString &foodName, double grams); + +private slots: + void onAddClicked(); + +private: + QLabel *nameLabel; + QTableWidget *nutrientsTable; + QPushButton *addButton; + FoodRepository repository; + + int currentFoodId; + QString currentFoodName; +}; + +#endif // DETAILSWIDGET_H diff --git a/include/widgets/mealwidget.h b/include/widgets/mealwidget.h new file mode 100644 index 0000000..cbf163b --- /dev/null +++ b/include/widgets/mealwidget.h @@ -0,0 +1,40 @@ +#ifndef MEALWIDGET_H +#define MEALWIDGET_H + +#include "db/foodrepository.h" +#include +#include +#include +#include +#include + +struct MealItem { + int foodId; + QString name; + double grams; + std::vector nutrients_100g; // Base nutrients +}; + +class MealWidget : public QWidget { + Q_OBJECT + +public: + explicit MealWidget(QWidget *parent = nullptr); + + void addFood(int foodId, const QString &foodName, double grams); + +private slots: + void clearMeal(); + +private: + void updateTotals(); + + QTableWidget *itemsTable; + QTableWidget *totalsTable; + QPushButton *clearButton; + + std::vector mealItems; + FoodRepository repository; +}; + +#endif // MEALWIDGET_H diff --git a/include/widgets/searchwidget.h b/include/widgets/searchwidget.h new file mode 100644 index 0000000..7c95bb3 --- /dev/null +++ b/include/widgets/searchwidget.h @@ -0,0 +1,32 @@ +#ifndef SEARCHWIDGET_H +#define SEARCHWIDGET_H + +#include "db/foodrepository.h" +#include +#include +#include +#include +#include + +class SearchWidget : public QWidget { + Q_OBJECT + +public: + explicit SearchWidget(QWidget *parent = nullptr); + +signals: + void foodSelected(int foodId, const QString &foodName); + +private slots: + void performSearch(); + void onRowDoubleClicked(int row, int column); + +private: + QLineEdit *searchInput; + QPushButton *searchButton; + QTableWidget *resultsTable; + FoodRepository repository; + QTimer *searchTimer; +}; + +#endif // SEARCHWIDGET_H diff --git a/nutra.desktop b/nutra.desktop new file mode 100644 index 0000000..099e32c --- /dev/null +++ b/nutra.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Nutra +Comment=Nutrition Tracker and USDA Database +Exec=nutra +Icon=nutra +Terminal=false +Type=Application +Categories=Utility;Database;Health; +StartupNotify=true +Keywords=nutrition;food;tracker;usda;diet; diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..d9595b2 --- /dev/null +++ b/resources.qrc @@ -0,0 +1,6 @@ + + + resources/nutrition_icon-no_bg.png + + + diff --git a/resources/nutrition_icon-no_bg.png b/resources/nutrition_icon-no_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..aa28f0c01538fa005a17d230582b9392dc133dbe GIT binary patch literal 51265 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 +#include +#include + +DatabaseManager &DatabaseManager::instance() { + static DatabaseManager instance; + return instance; +} + +DatabaseManager::DatabaseManager() = default; + +DatabaseManager::~DatabaseManager() { + if (m_db.isOpen()) { + m_db.close(); + } +} + +bool DatabaseManager::connect(const QString &path) { + if (m_db.isOpen()) { + return true; + } + + if (!QFileInfo::exists(path)) { + qCritical() << "Database file not found:" << path; + return false; + } + + m_db = QSqlDatabase::addDatabase("QSQLITE"); + m_db.setDatabaseName(path); + m_db.setConnectOptions("QSQLITE_OPEN_READONLY"); + + if (!m_db.open()) { + qCritical() << "Error opening database:" << m_db.lastError().text(); + return false; + } + + return true; +} + +bool DatabaseManager::isOpen() const { return m_db.isOpen(); } + +QSqlDatabase DatabaseManager::database() const { return m_db; } diff --git a/src/db/foodrepository.cpp b/src/db/foodrepository.cpp new file mode 100644 index 0000000..3d18417 --- /dev/null +++ b/src/db/foodrepository.cpp @@ -0,0 +1,177 @@ +#include "db/foodrepository.h" +#include "db/databasemanager.h" +#include +#include +#include +#include +#include + +FoodRepository::FoodRepository() {} + +#include "utils/string_utils.h" +#include + +// ... + +void FoodRepository::ensureCacheLoaded() { + if (m_cacheLoaded) + return; + + QSqlDatabase db = DatabaseManager::instance().database(); + if (!db.isOpen()) + return; + + // 1. Load Food Items + QSqlQuery query("SELECT id, long_desc, fdgrp_id FROM food_des", db); + std::map nutrientCounts; + + // 2. Load Nutrient Counts (Bulk) + QSqlQuery countQuery( + "SELECT food_id, count(*) FROM nut_data GROUP BY food_id", db); + while (countQuery.next()) { + nutrientCounts[countQuery.value(0).toInt()] = countQuery.value(1).toInt(); + } + + while (query.next()) { + FoodItem item; + item.id = query.value(0).toInt(); + item.description = query.value(1).toString(); + item.foodGroupId = query.value(2).toInt(); + + // Set counts from map (default 0 if not found) + auto it = nutrientCounts.find(item.id); + item.nutrientCount = (it != nutrientCounts.end()) ? it->second : 0; + + item.aminoCount = 0; // TODO: Implement specific counts if needed + item.flavCount = 0; + item.score = 0; + m_cache.push_back(item); + } + m_cacheLoaded = true; +} + +std::vector FoodRepository::searchFoods(const QString &query) { + ensureCacheLoaded(); + std::vector results; + + if (query.trimmed().isEmpty()) + return results; + + // Calculate scores + // create a temporary list of pointers or indices to sort? + // Copying might be expensive if cache is huge (8k items is fine though) + + // Let's iterate and keep top matches. + struct ScoredItem { + const FoodItem *item; + int score; + }; + std::vector scoredItems; + scoredItems.reserve(m_cache.size()); + + for (const auto &item : m_cache) { + int score = Utils::calculateFuzzyScore(query, item.description); + if (score > 40) { // Threshold + scoredItems.push_back({&item, score}); + } + } + + // Sort by score desc + std::sort(scoredItems.begin(), scoredItems.end(), + [](const ScoredItem &a, const ScoredItem &b) { + return a.score > b.score; + }); + + // Take top 100 + int count = 0; + std::vector resultIds; + std::map idToIndex; + + for (const auto &si : scoredItems) { + if (count >= 100) + break; + FoodItem res = *si.item; + res.score = si.score; + // We will populate nutrients shortly + results.push_back(res); + resultIds.push_back(res.id); + idToIndex[res.id] = count; + count++; + } + + // Batch fetch nutrients for these results + if (!resultIds.empty()) { + QSqlDatabase db = DatabaseManager::instance().database(); + QStringList idStrings; + for (int id : resultIds) + idStrings << QString::number(id); + + QString sql = + QString("SELECT n.food_id, n.nutr_id, n.nutr_val, d.nutr_desc, d.unit " + "FROM nut_data n " + "JOIN nutr_def d ON n.nutr_id = d.id " + "WHERE n.food_id IN (%1)") + .arg(idStrings.join(",")); + + QSqlQuery nutQuery(sql, db); + while (nutQuery.next()) { + int fid = nutQuery.value(0).toInt(); + Nutrient nut; + nut.id = nutQuery.value(1).toInt(); + nut.amount = nutQuery.value(2).toDouble(); + nut.description = nutQuery.value(3).toString(); + nut.unit = nutQuery.value(4).toString(); + nut.rdaPercentage = 0.0; + + if (idToIndex.count(fid) != 0U) { + results[idToIndex[fid]].nutrients.push_back(nut); + } + } + + // Update counts based on actual data + for (auto &res : results) { + res.nutrientCount = static_cast(res.nutrients.size()); + // TODO: Logic for amino/flav counts if we have ranges of IDs + } + } + + return results; +} + +std::vector FoodRepository::getFoodNutrients(int foodId) { + std::vector results; + QSqlDatabase db = DatabaseManager::instance().database(); + + if (!db.isOpen()) + return results; + + QSqlQuery query(db); + if (!query.prepare("SELECT n.nutr_id, n.nutr_val, d.nutr_desc, d.unit " + "FROM nut_data n " + "JOIN nutr_def d ON n.nutr_id = d.id " + "WHERE n.food_id = ?")) { + + qCritical() << "Prepare failed:" << query.lastError().text(); + return results; + } + + query.bindValue(0, foodId); + + if (query.exec()) { + while (query.next()) { + Nutrient nut; + nut.id = query.value(0).toInt(); + nut.amount = query.value(1).toDouble(); + nut.description = query.value(2).toString(); + nut.unit = query.value(3).toString(); + nut.rdaPercentage = 0.0; + + results.push_back(nut); + } + + } else { + qCritical() << "Nutrient query failed:" << query.lastError().text(); + } + + return results; +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a6857d0 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,68 @@ +#include "db/databasemanager.h" +#include "mainwindow.h" +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); + QApplication::setApplicationName("Nutra"); + QApplication::setOrganizationName("NutraTech"); + QApplication::setWindowIcon(QIcon(":/resources/nutrition_icon-no_bg.png")); + + // Connect to database + // Search order: + // 1. Environment variable NUTRA_DB_PATH + // 2. Local user data: ~/.local/share/nutra/usda.sqlite3 + // 3. System install: /usr/local/share/nutra/usda.sqlite3 + // 4. System install: /usr/share/nutra/usda.sqlite3 + // 5. Legacy: ~/.nutra/usda.sqlite3 + + QStringList searchPaths; + QString envPath = qEnvironmentVariable("NUTRA_DB_PATH"); + if (!envPath.isEmpty()) + searchPaths << envPath; + + searchPaths << QStandardPaths::locate(QStandardPaths::AppDataLocation, + "usda.sqlite3", + QStandardPaths::LocateFile); + searchPaths << QDir::homePath() + "/.local/share/nutra/usda.sqlite3"; + searchPaths << "/usr/local/share/nutra/usda.sqlite3"; + searchPaths << "/usr/share/nutra/usda.sqlite3"; + searchPaths << QDir::homePath() + "/.nutra/usda.sqlite3"; + + QString dbPath; + for (const QString &path : searchPaths) { + if (!path.isEmpty() && QFileInfo::exists(path)) { + dbPath = path; + break; + } + } + + if (dbPath.isEmpty()) { + // If not found, default to XDG AppData location for error message/setup + // But we can't create it here. + dbPath = QDir::homePath() + "/.nutra/usda.sqlite3"; // Fallback default + qWarning() << "Database not found in standard locations."; + } + + if (!DatabaseManager::instance().connect(dbPath)) { + QString errorMsg = + QString("Failed to connect to database at:\n%1\n\nPlease ensure the " + "database file exists or reinstall the application.") + .arg(dbPath); + qCritical() << errorMsg; + QMessageBox::critical(nullptr, "Database Error", errorMsg); + return 1; + } + qDebug() << "Connected to database at:" << dbPath; + + MainWindow window; + window.show(); + + return QApplication::exec(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..4cf1803 --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,52 @@ +#include "mainwindow.h" +#include + +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setupUi(); } + +MainWindow::~MainWindow() = default; + +void MainWindow::setupUi() { + setWindowTitle("Nutrient Coach"); + setWindowIcon(QIcon(":/resources/nutrition_icon-no_bg.png")); + resize(1000, 700); + + auto *centralWidget = new QWidget(this); + setCentralWidget(centralWidget); + + auto *mainLayout = new QVBoxLayout(centralWidget); + + tabs = new QTabWidget(this); + mainLayout->addWidget(tabs); + + // Search Tab + searchWidget = new SearchWidget(this); + tabs->addTab(searchWidget, "Search Foods"); + + // Connect signal + connect(searchWidget, &SearchWidget::foodSelected, this, + [=](int foodId, const QString &foodName) { + qDebug() << "Selected food:" << foodName << "(" << foodId << ")"; + detailsWidget->loadFood(foodId, foodName); + tabs->setCurrentWidget(detailsWidget); + }); + + // Analysis Tab + detailsWidget = new DetailsWidget(this); + tabs->addTab(detailsWidget, "Analyze"); + + // Meal Tab + mealWidget = new MealWidget(this); + tabs->addTab(mealWidget, "Meal Tracker"); + + // Connect Analysis -> Meal + connect(detailsWidget, &DetailsWidget::addToMeal, this, + [=](int foodId, const QString &foodName, double grams) { + mealWidget->addFood(foodId, foodName, grams); + // Optional: switch tab? + // tabs->setCurrentWidget(mealWidget); + }); +} diff --git a/src/utils/string_utils.cpp b/src/utils/string_utils.cpp new file mode 100644 index 0000000..a6d1635 --- /dev/null +++ b/src/utils/string_utils.cpp @@ -0,0 +1,112 @@ +#include "utils/string_utils.h" +#include +#include +#include +#include // Required for std::max +#include + +#ifndef QT_VERSION_CHECK +#define QT_VERSION_CHECK(major, minor, patch) \ + ((major << 16) | (minor << 8) | (patch)) +#endif + +namespace Utils { + +int levenshteinDistance(const QString &s1, const QString &s2) { + const auto m = s1.length(); + const auto n = s2.length(); + + std::vector> dp(m + 1, std::vector(n + 1)); + + for (int i = 0; i <= m; ++i) { + dp[i][0] = i; + } + for (int j = 0; j <= n; ++j) { + dp[0][j] = j; + } + + for (int i = 1; i <= m; ++i) { + for (int j = 1; j <= n; ++j) { + if (s1[i - 1] == s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + std::min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}); + } + } + } + + return dp[m][n]; +} + +int calculateFuzzyScore(const QString &query, const QString &target) { + if (query.isEmpty()) { + return 0; + } + if (target.isEmpty()) { + return 0; + } + + QString q = query.toLower(); + QString t = target.toLower(); + + // 1. Exact match bonus + if (t == q) { + return 100; + } + + // 2. Contains match bonus (very strong signal) + if (t.contains(q)) { + return 90; // Base score for containing the string + } + + // 3. Token-based matching (handling "grass fed" vs "beef, grass-fed") + static const QRegularExpression regex("[\\s,-]+"); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto behavior = Qt::SkipEmptyParts; +#else + auto behavior = QString::SkipEmptyParts; +#endif + QStringList queryTokens = q.split(regex, behavior); + QStringList targetTokens = t.split(regex, behavior); + + int totalScore = 0; + int matchedTokens = 0; + + for (const QString &qToken : queryTokens) { + int maxTokenScore = 0; + for (const QString &tToken : targetTokens) { + int dist = levenshteinDistance(qToken, tToken); + int maxLen = static_cast(std::max(qToken.length(), tToken.length())); + if (maxLen == 0) + continue; + + int score = 0; + if (tToken.startsWith(qToken)) { + score = 95; // Prefix match is very good + } else { + double ratio = 1.0 - (static_cast(dist) / maxLen); + score = static_cast(ratio * 100); + } + + maxTokenScore = std::max(maxTokenScore, score); + } + totalScore += maxTokenScore; + if (maxTokenScore > 60) + matchedTokens++; + } + + if (queryTokens.isEmpty()) { + return 0; + } + + int averageScore = static_cast(totalScore / queryTokens.size()); + + // Penalize if not all tokens matched somewhat well + if (matchedTokens < queryTokens.size()) { + averageScore -= 20; + } + + return std::max(0, averageScore); +} + +} // namespace Utils diff --git a/src/widgets/detailswidget.cpp b/src/widgets/detailswidget.cpp new file mode 100644 index 0000000..c775d05 --- /dev/null +++ b/src/widgets/detailswidget.cpp @@ -0,0 +1,63 @@ +#include "widgets/detailswidget.h" +#include +#include +#include +#include + +DetailsWidget::DetailsWidget(QWidget *parent) + : QWidget(parent), currentFoodId(-1) { + auto *layout = new QVBoxLayout(this); + + // Header + auto *headerLayout = new QHBoxLayout(); + nameLabel = new QLabel("No food selected", this); + QFont font = nameLabel->font(); + font.setPointSize(14); + font.setBold(true); + nameLabel->setFont(font); + + addButton = new QPushButton("Add to Meal", this); + addButton->setEnabled(false); + connect(addButton, &QPushButton::clicked, this, &DetailsWidget::onAddClicked); + + headerLayout->addWidget(nameLabel); + headerLayout->addStretch(); + headerLayout->addWidget(addButton); + layout->addLayout(headerLayout); + + // Nutrients Table + nutrientsTable = new QTableWidget(this); + nutrientsTable->setColumnCount(3); + nutrientsTable->setHorizontalHeaderLabels({"Nutrient", "Amount", "Unit"}); + nutrientsTable->horizontalHeader()->setSectionResizeMode( + 0, QHeaderView::Stretch); + nutrientsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + layout->addWidget(nutrientsTable); +} + +void DetailsWidget::loadFood(int foodId, const QString &foodName) { + currentFoodId = foodId; + currentFoodName = foodName; + nameLabel->setText(foodName + QString(" (ID: %1)").arg(foodId)); + addButton->setEnabled(true); + + nutrientsTable->setRowCount(0); + + std::vector nutrients = repository.getFoodNutrients(foodId); + + nutrientsTable->setRowCount(static_cast(nutrients.size())); + for (int i = 0; i < static_cast(nutrients.size()); ++i) { + const auto &nut = nutrients[i]; + nutrientsTable->setItem(i, 0, new QTableWidgetItem(nut.description)); + nutrientsTable->setItem(i, 1, + new QTableWidgetItem(QString::number(nut.amount))); + nutrientsTable->setItem(i, 2, new QTableWidgetItem(nut.unit)); + } +} + +void DetailsWidget::onAddClicked() { + if (currentFoodId != -1) { + // Default 100g + emit addToMeal(currentFoodId, currentFoodName, 100.0); + } +} diff --git a/src/widgets/mealwidget.cpp b/src/widgets/mealwidget.cpp new file mode 100644 index 0000000..9078ebd --- /dev/null +++ b/src/widgets/mealwidget.cpp @@ -0,0 +1,98 @@ +#include "widgets/mealwidget.h" +#include +#include +#include +#include +#include + +MealWidget::MealWidget(QWidget *parent) : QWidget(parent) { + auto *layout = new QVBoxLayout(this); + + // Items List + layout->addWidget(new QLabel("Meal Composition", this)); + itemsTable = new QTableWidget(this); + itemsTable->setColumnCount(3); + itemsTable->setHorizontalHeaderLabels({"Food", "Grams", "Calories"}); + itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + layout->addWidget(itemsTable); + + // Controls + clearButton = new QPushButton("Clear Meal", this); + connect(clearButton, &QPushButton::clicked, this, &MealWidget::clearMeal); + layout->addWidget(clearButton); + + // Totals + layout->addWidget(new QLabel("Total Nutrition", this)); + totalsTable = new QTableWidget(this); + totalsTable->setColumnCount(3); + totalsTable->setHorizontalHeaderLabels({"Nutrient", "Total", "Unit"}); + totalsTable->horizontalHeader()->setSectionResizeMode(0, + QHeaderView::Stretch); + layout->addWidget(totalsTable); +} + +void MealWidget::addFood(int foodId, const QString &foodName, double grams) { + std::vector baseNutrients = repository.getFoodNutrients(foodId); + + MealItem item; + item.foodId = foodId; + item.name = foodName; + item.grams = grams; + item.nutrients_100g = baseNutrients; + + mealItems.push_back(item); + + // Update Items Table + int row = itemsTable->rowCount(); + itemsTable->insertRow(row); + itemsTable->setItem(row, 0, new QTableWidgetItem(foodName)); + itemsTable->setItem(row, 1, new QTableWidgetItem(QString::number(grams))); + + // Calculate Calories (ID 208 usually, or find by name?) + // repository returns IDs based on DB. 208 is KCAL in SR28. + double kcal = 0; + for (const auto &nut : baseNutrients) { + if (nut.id == 208) { + kcal = (nut.amount * grams) / 100.0; + break; + } + } + itemsTable->setItem(row, 2, + new QTableWidgetItem(QString::number(kcal, 'f', 1))); + + updateTotals(); +} + +void MealWidget::clearMeal() { + mealItems.clear(); + itemsTable->setRowCount(0); + updateTotals(); +} + +void MealWidget::updateTotals() { + std::map totals; // id -> amount + std::map units; + std::map names; + + for (const auto &item : mealItems) { + double scale = item.grams / 100.0; + for (const auto &nut : item.nutrients_100g) { + totals[nut.id] += nut.amount * scale; + names.try_emplace(nut.id, nut.description); + units.try_emplace(nut.id, nut.unit); + } + } + + totalsTable->setRowCount(static_cast(totals.size())); + int row = 0; + for (const auto &pair : totals) { + int nid = pair.first; + double amount = pair.second; + + totalsTable->setItem(row, 0, new QTableWidgetItem(names[nid])); + totalsTable->setItem(row, 1, + new QTableWidgetItem(QString::number(amount, 'f', 2))); + totalsTable->setItem(row, 2, new QTableWidgetItem(units[nid])); + row++; + } +} diff --git a/src/widgets/searchwidget.cpp b/src/widgets/searchwidget.cpp new file mode 100644 index 0000000..a035191 --- /dev/null +++ b/src/widgets/searchwidget.cpp @@ -0,0 +1,85 @@ +#include "widgets/searchwidget.h" +#include +#include +#include +#include + +SearchWidget::SearchWidget(QWidget *parent) : QWidget(parent) { + auto *layout = new QVBoxLayout(this); + + // Search bar + auto *searchLayout = new QHBoxLayout(); + searchInput = new QLineEdit(this); + searchInput->setPlaceholderText("Search for food..."); + + searchTimer = new QTimer(this); + searchTimer->setSingleShot(true); + searchTimer->setInterval(600); // 600ms debounce + + connect(searchInput, &QLineEdit::textChanged, this, + [=]() { searchTimer->start(); }); + connect(searchTimer, &QTimer::timeout, this, &SearchWidget::performSearch); + connect(searchInput, &QLineEdit::returnPressed, this, + &SearchWidget::performSearch); + + searchButton = new QPushButton("Search", this); + connect(searchButton, &QPushButton::clicked, this, + &SearchWidget::performSearch); + + searchLayout->addWidget(searchInput); + searchLayout->addWidget(searchButton); + layout->addLayout(searchLayout); + + // Results table + resultsTable = new QTableWidget(this); + resultsTable->setColumnCount(7); + resultsTable->setHorizontalHeaderLabels( + {"ID", "Description", "Group", "Nutr", "Amino", "Flav", "Score"}); + + resultsTable->horizontalHeader()->setSectionResizeMode(1, + QHeaderView::Stretch); + resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows); + resultsTable->setSelectionMode(QAbstractItemView::SingleSelection); + resultsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + connect(resultsTable, &QTableWidget::cellDoubleClicked, this, + &SearchWidget::onRowDoubleClicked); + + layout->addWidget(resultsTable); +} + +void SearchWidget::performSearch() { + QString query = searchInput->text().trimmed(); + if (query.length() < 2) + return; + + resultsTable->setRowCount(0); + + std::vector results = repository.searchFoods(query); + + resultsTable->setRowCount(static_cast(results.size())); + for (int i = 0; i < static_cast(results.size()); ++i) { + 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, 3, new QTableWidgetItem(QString::number(item.nutrientCount))); + resultsTable->setItem( + i, 4, new QTableWidgetItem(QString::number(item.aminoCount))); + resultsTable->setItem( + i, 5, new QTableWidgetItem(QString::number(item.flavCount))); + resultsTable->setItem(i, 6, + new QTableWidgetItem(QString::number(item.score))); + } +} + +void SearchWidget::onRowDoubleClicked(int row, int column) { + Q_UNUSED(column); + QTableWidgetItem *idItem = resultsTable->item(row, 0); + QTableWidgetItem *descItem = resultsTable->item(row, 1); + + if (idItem != nullptr && descItem != nullptr) { + emit foodSelected(idItem->text().toInt(), descItem->text()); + } +} diff --git a/tests/test_foodrepository.cpp b/tests/test_foodrepository.cpp new file mode 100644 index 0000000..60a11c2 --- /dev/null +++ b/tests/test_foodrepository.cpp @@ -0,0 +1,58 @@ +#include "db/databasemanager.h" +#include "db/foodrepository.h" +#include +#include +#include + +class TestFoodRepository : public QObject { + Q_OBJECT + +private slots: + void initTestCase() { + // Setup DB connection + // Allow override via environment variable for CI/testing + QString envPath = qgetenv("NUTRA_DB_PATH"); + QString dbPath = + envPath.isEmpty() ? QDir::homePath() + "/.nutra/usda.sqlite3" : envPath; + + if (!QFileInfo::exists(dbPath)) { + QSKIP("Database file not found (NUTRA_DB_PATH or ~/.nutra/usda.sqlite3). " + "Skipping DB tests."); + } + + bool connected = DatabaseManager::instance().connect(dbPath); + QVERIFY2(connected, "Failed to connect to database"); + } + + void testSearchFoods() { + FoodRepository repo; + auto results = repo.searchFoods("apple"); + QVERIFY2(!results.empty(), "Search should return results for 'apple'"); + bool found = false; + for (const auto &item : results) { + if (item.description.contains("Apple", Qt::CaseInsensitive)) { + found = true; + break; + } + } + QVERIFY2(found, "Search results should contain 'Apple'"); + } + + void testGetFoodNutrients() { + FoodRepository repo; + // Known ID for "Apples, raw, with skin" might be 9003 in SR28, but let's + // search first or pick a known one if we knew it. Let's just use the first + // result from search. + auto results = repo.searchFoods("apple"); + if (results.empty()) + QSKIP("No foods found to test nutrients"); + + int foodId = results[0].id; + auto nutrients = repo.getFoodNutrients(foodId); + QVERIFY2(!nutrients.empty(), + "Nutrients should not be empty for a valid food"); + } +}; + +QTEST_MAIN(TestFoodRepository) +#include "test_foodrepository.moc" diff --git a/usdasqlite b/usdasqlite new file mode 160000 index 0000000..4644288 --- /dev/null +++ b/usdasqlite @@ -0,0 +1 @@ +Subproject commit 4644288e1cfc0bac44dd3a0bfdaafa5bd72cf819 -- 2.52.0