diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..3820a95
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/app/.metadata b/app/.metadata
new file mode 100644
index 0000000..fdcdbd8
--- /dev/null
+++ b/app/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "c9a6c484230f8b5e408ec57be1ef71dee1e77020"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
+ base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
+ - platform: android
+ create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
+ base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000..cf54a36
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,17 @@
+# diet_guard_app
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
+- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml
new file mode 100644
index 0000000..bac0810
--- /dev/null
+++ b/app/analysis_options.yaml
@@ -0,0 +1,32 @@
+include: package:very_good_analysis/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
+ errors:
+ # Promote key lints to errors so they fail CI, not just warn.
+ missing_required_param: error
+ unnecessary_null_comparison: error
+ dead_code: error
+ invalid_annotation_target: error
+ exclude:
+ - build/**
+ - "**/*.g.dart"
+ - "**/*.freezed.dart"
+ - "test/**"
+
+linter:
+ rules:
+ # very_good_analysis enables most rules; add extras it doesn't include.
+ always_use_package_imports: true
+ avoid_print: true
+ avoid_relative_lib_imports: true
+ cancel_subscriptions: true
+ close_sinks: true
+ comment_references: false # conflicts with typical Flutter API docs style
+ directives_ordering: true
+ lines_longer_than_80_chars: true
+ public_member_api_docs: true
+ unawaited_futures: true
diff --git a/app/android/.gitignore b/app/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/app/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/app/android/app/build.gradle.kts b/app/android/app/build.gradle.kts
new file mode 100644
index 0000000..1e1a8a0
--- /dev/null
+++ b/app/android/app/build.gradle.kts
@@ -0,0 +1,45 @@
+plugins {
+ id("com.android.application")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.kuhy.diet_guard_app"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.kuhy.diet_guard_app"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/app/android/app/src/debug/AndroidManifest.xml b/app/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/app/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9ffa9c1
--- /dev/null
+++ b/app/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/android/app/src/main/kotlin/com/kuhy/diet_guard_app/MainActivity.kt b/app/android/app/src/main/kotlin/com/kuhy/diet_guard_app/MainActivity.kt
new file mode 100644
index 0000000..852aa10
--- /dev/null
+++ b/app/android/app/src/main/kotlin/com/kuhy/diet_guard_app/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.kuhy.diet_guard_app
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/app/android/app/src/main/res/drawable-v21/launch_background.xml b/app/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/app/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/app/android/app/src/main/res/drawable/launch_background.xml b/app/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/app/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/android/app/src/main/res/values-night/styles.xml b/app/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/app/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/app/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/app/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/app/android/app/src/profile/AndroidManifest.xml b/app/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/app/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/app/android/build.gradle.kts b/app/android/build.gradle.kts
new file mode 100644
index 0000000..dbee657
--- /dev/null
+++ b/app/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/app/android/gradle.properties b/app/android/gradle.properties
new file mode 100644
index 0000000..e96108c
--- /dev/null
+++ b/app/android/gradle.properties
@@ -0,0 +1,6 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+# This newDsl flag was added by the Flutter template
+android.newDsl=false
+# This builtInKotlin flag was added by the Flutter template
+android.builtInKotlin=false
diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..2d428bf
--- /dev/null
+++ b/app/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
diff --git a/app/android/settings.gradle.kts b/app/android/settings.gradle.kts
new file mode 100644
index 0000000..c21f0c5
--- /dev/null
+++ b/app/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "9.0.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.3.20" apply false
+}
+
+include(":app")
diff --git a/app/lib/main.dart b/app/lib/main.dart
new file mode 100644
index 0000000..0016e37
--- /dev/null
+++ b/app/lib/main.dart
@@ -0,0 +1,30 @@
+/// App entry point: initializes local storage services, then shows the
+/// primary meal-logging screen.
+library;
+
+import 'package:diet_guard_app/screens/log_meal_screen.dart';
+import 'package:diet_guard_app/services/foodbank_service.dart';
+import 'package:diet_guard_app/services/log_storage_service.dart';
+import 'package:flutter/material.dart';
+
+Future main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ await LogStorageService.init();
+ await FoodBankService.init();
+ runApp(const DietGuardApp());
+}
+
+/// Root widget for the Diet Guard companion app.
+class DietGuardApp extends StatelessWidget {
+ /// Creates the [DietGuardApp] root widget.
+ const DietGuardApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Diet Guard',
+ theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
+ home: const LogMealScreen(),
+ );
+ }
+}
diff --git a/app/lib/models/food_bank_record.dart b/app/lib/models/food_bank_record.dart
new file mode 100644
index 0000000..76a7142
--- /dev/null
+++ b/app/lib/models/food_bank_record.dart
@@ -0,0 +1,73 @@
+/// One entry in the local food bank (autocomplete index), mirroring
+/// diet_guard's `_foodbank.BankRecord`.
+library;
+
+/// A previously-logged food's remembered macros and use count.
+///
+/// Mirrors `_foodbank.py`'s on-disk shape: `{desc, kcal, protein_g,
+/// carbs_g, fat_g, grams, count, components?}`. Unlike [FoodEntry], a
+/// composite record's `components` here are bare names (the bank is an
+/// autocomplete index, not the source of truth for component macros --
+/// those live on the log entry itself, see `MealComponent`).
+class FoodBankRecord {
+ /// Creates a [FoodBankRecord] from its stored fields.
+ const FoodBankRecord({
+ required this.desc,
+ required this.kcal,
+ required this.proteinG,
+ required this.carbsG,
+ required this.fatG,
+ required this.grams,
+ required this.count,
+ this.components,
+ });
+
+ /// Builds a [FoodBankRecord] from its JSON map representation.
+ factory FoodBankRecord.fromJson(Map json) =>
+ FoodBankRecord(
+ desc: json['desc'] as String? ?? '',
+ kcal: (json['kcal'] as num?)?.toDouble() ?? 0,
+ proteinG: (json['protein_g'] as num?)?.toDouble() ?? 0,
+ carbsG: (json['carbs_g'] as num?)?.toDouble() ?? 0,
+ fatG: (json['fat_g'] as num?)?.toDouble() ?? 0,
+ grams: (json['grams'] as num?)?.toDouble() ?? 0,
+ count: (json['count'] as num?)?.toDouble() ?? 0,
+ components: (json['components'] as List?)?.cast(),
+ );
+
+ /// The food or meal's display name, as the user typed it.
+ final String desc;
+
+ /// Calories per the stored portion.
+ final double kcal;
+
+ /// Protein in grams.
+ final double proteinG;
+
+ /// Carbohydrate in grams.
+ final double carbsG;
+
+ /// Fat in grams.
+ final double fatG;
+
+ /// Portion weight in grams.
+ final double grams;
+
+ /// Number of times this food has been logged (ranks staples first).
+ final double count;
+
+ /// Component names, for a composite meal record only.
+ final List? components;
+
+ /// Returns this record as a JSON-ready map with snake_case keys.
+ Map toJson() => {
+ 'desc': desc,
+ 'kcal': kcal,
+ 'protein_g': proteinG,
+ 'carbs_g': carbsG,
+ 'fat_g': fatG,
+ 'grams': grams,
+ 'count': count,
+ if (components != null) 'components': components,
+ };
+}
diff --git a/app/lib/models/food_entry.dart b/app/lib/models/food_entry.dart
new file mode 100644
index 0000000..248e462
--- /dev/null
+++ b/app/lib/models/food_entry.dart
@@ -0,0 +1,165 @@
+/// A single logged meal entry, mirroring one `food_log.json` array element.
+library;
+
+import 'package:diet_guard_app/models/meal_component.dart';
+
+/// One logged meal, as stored in `food_log.json` under its date key.
+///
+/// Field names and shapes mirror diet_guard's `_state.log_meal` entry
+/// exactly, so this app's local storage *is* the wire format -- no
+/// translation layer is needed when syncing with the PC app.
+class FoodEntry {
+ /// Creates a [FoodEntry] from its stored fields.
+ const FoodEntry({
+ required this.time,
+ required this.desc,
+ required this.grams,
+ required this.kcal,
+ required this.proteinG,
+ required this.carbsG,
+ required this.fatG,
+ required this.source,
+ this.id,
+ this.slot,
+ this.hmac,
+ this.components,
+ this.deleted = false,
+ this.imagePath,
+ });
+
+ /// Builds a [FoodEntry] from its JSON map representation.
+ ///
+ /// Missing/non-numeric macro fields default to 0, mirroring
+ /// `_state._entry_float`'s tolerance of a hand-edited or partial entry.
+ factory FoodEntry.fromJson(Map json) => FoodEntry(
+ id: json['id'] as String?,
+ time: json['time'] as String? ?? '',
+ desc: json['desc'] as String? ?? '',
+ grams: (json['grams'] as num?)?.toDouble() ?? 0,
+ kcal: (json['kcal'] as num?)?.toDouble() ?? 0,
+ proteinG: (json['protein_g'] as num?)?.toDouble() ?? 0,
+ carbsG: (json['carbs_g'] as num?)?.toDouble() ?? 0,
+ fatG: (json['fat_g'] as num?)?.toDouble() ?? 0,
+ source: json['source'] as String? ?? 'manual',
+ slot: json['slot'] as int?,
+ hmac: json['hmac'] as String?,
+ components: (json['components'] as List?)
+ ?.cast