From c458e0807550383ca59f8742ce988818ccb5b36c Mon Sep 17 00:00:00 2001 From: genki <123@1234.com> Date: Wed, 24 Dec 2025 22:48:34 -0500 Subject: [PATCH] Correct schema Meaningful queries Proper transactional reads --- app/build.gradle.kts | 97 +++++++++---------- .../sherpai2/data/local/AppDatabase.kt | 47 +++++++++ .../sherpai2/data/local/dao/EventDao.kt | 26 +++++ .../data/local/dao/ImageAggregateDao.kt | 25 +++++ .../sherpai2/data/local/dao/ImageDao.kt | 56 +++++++++++ .../sherpai2/data/local/dao/ImageEventDao.kt | 23 +++++ .../sherpai2/data/local/dao/ImagePersonDao.kt | 25 +++++ .../sherpai2/data/local/dao/ImageTagDao.kt | 41 ++++++++ .../sherpai2/data/local/dao/PersonDao.kt | 24 +++++ .../sherpai2/data/local/dao/TagDao.kt | 24 +++++ .../sherpai2/data/local/entity/EventEntity.kt | 44 +++++++++ .../sherpai2/data/local/entity/ImageEntity.kt | 55 +++++++++++ .../data/local/entity/ImageEventEntity.kt | 42 ++++++++ .../data/local/entity/ImagePersonEntity.kt | 40 ++++++++ .../data/local/entity/ImageTagEntity.kt | 56 +++++++++++ .../data/local/entity/PersonsEntity.kt | 30 ++++++ .../sherpai2/data/local/entity/TagEntity.kt | 30 ++++++ .../data/local/model/ImageWithEverything.kt | 29 ++++++ .../data/local/model/ImageWithTags.kt | 18 ++++ .../placeholder/sherpai2/di/DatabaseModule.kt | 28 ++++++ build.gradle.kts | 1 + gradle/libs.versions.toml | 62 +++++++++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 23 files changed, 759 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/AppDatabase.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/dao/EventDao.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageAggregateDao.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageDao.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageEventDao.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImagePersonDao.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageTagDao.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/dao/PersonDao.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/dao/TagDao.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/entity/EventEntity.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageEntity.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageEventEntity.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImagePersonEntity.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageTagEntity.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/entity/PersonsEntity.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/entity/TagEntity.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/model/ImageWithEverything.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/data/local/model/ImageWithTags.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/di/DatabaseModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4044103..c831952 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,77 +1,76 @@ -// build.gradle.kts (Module: :app) - plugins { - // 1. Core Android and Kotlin plugins (MUST be first) - id("com.android.application") - kotlin("android") - - id("org.jetbrains.kotlin.plugin.compose") // Note: No version is specified here + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt.android) } android { - // 2. Android Configuration namespace = "com.placeholder.sherpai2" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.placeholder.sherpai2" minSdk = 24 - targetSdk = 34 + targetSdk = 35 versionCode = 1 versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - // 3. Kotlin & Java Settings - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" } - // 4. Jetpack Compose Configuration (Crucial!) buildFeatures { compose = true } - - composeOptions { - kotlinCompilerExtensionVersion = "1.5.8" // Must match your Kotlin version - } } dependencies { - // --- CORE ANDROID & LIFECYCLE --- - implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") - implementation("androidx.activity:activity-compose:1.8.2") // Fixes 'activity' ref error + // AndroidX & Lifecycle + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.activity.compose) - // --- JETPACK COMPOSE UI (Material 3) --- - implementation("androidx.compose.ui:ui") - implementation("androidx.compose.ui:ui-graphics") - implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.compose.material3:material3") // Fixes 'material3' ref error + // Compose (Using BOM) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons) - // --- COMPOSE ICONS (Fixes 'material' and 'Icons' ref errors) --- - // Uses direct string to avoid Version Catalog conflicts - implementation("androidx.compose.material:material-icons-extended:1.6.0") + // Navigation & Hilt Integration + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) - // --- STATE MANAGEMENT / COROUTINES --- - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + // Images + implementation(libs.coil.compose) - // --- TESTING --- - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation("androidx.compose.ui:ui-test-junit4") - debugImplementation("androidx.compose.ui:ui-tooling") - debugImplementation("androidx.compose.ui:ui-test-manifest") + // Tooling (Fixed to use TOML alias) + debugImplementation(libs.androidx.compose.ui.tooling) - implementation("androidx.compose.foundation:foundation:1.6.0") // Use your current Compose version - implementation("androidx.compose.material3:material3:1.2.1") // <-- Fix/Reconfirm Material 3 - - implementation("io.coil-kt:coil-compose:2.6.0") + //backend2 + //room addon + implementation(libs.room.runtime) + implementation(libs.room.ktx) + ksp(libs.room.compiler) } \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/AppDatabase.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/AppDatabase.kt new file mode 100644 index 0000000..c42000a --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/AppDatabase.kt @@ -0,0 +1,47 @@ +package com.placeholder.sherpai2.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.placeholder.sherpai2.data.local.dao.EventDao +import com.placeholder.sherpai2.data.local.dao.ImageAggregateDao +import com.placeholder.sherpai2.data.local.dao.ImageDao +import com.placeholder.sherpai2.data.local.dao.ImageEventDao +import com.placeholder.sherpai2.data.local.dao.ImagePersonDao +import com.placeholder.sherpai2.data.local.dao.ImageTagDao +import com.placeholder.sherpai2.data.local.dao.PersonDao +import com.placeholder.sherpai2.data.local.dao.TagDao +import com.placeholder.sherpai2.data.local.entity.EventEntity +import com.placeholder.sherpai2.data.local.entity.ImageEntity +import com.placeholder.sherpai2.data.local.entity.ImageEventEntity +import com.placeholder.sherpai2.data.local.entity.ImagePersonEntity +import com.placeholder.sherpai2.data.local.entity.ImageTagEntity +import com.placeholder.sherpai2.data.local.entity.PersonEntity +import com.placeholder.sherpai2.data.local.entity.TagEntity + +@Database( + entities = [ + ImageEntity::class, + TagEntity::class, + PersonEntity::class, + EventEntity::class, + ImageTagEntity::class, + ImagePersonEntity::class, + ImageEventEntity::class + ], + version = 1, + exportSchema = true +) + +abstract class AppDatabase : RoomDatabase() { + + abstract fun imageDao(): ImageDao + abstract fun tagDao(): TagDao + abstract fun personDao(): PersonDao + abstract fun eventDao(): EventDao + + abstract fun imageTagDao(): ImageTagDao + abstract fun imagePersonDao(): ImagePersonDao + abstract fun imageEventDao(): ImageEventDao + + abstract fun imageAggregateDao(): ImageAggregateDao +} \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/EventDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/EventDao.kt new file mode 100644 index 0000000..462a294 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/EventDao.kt @@ -0,0 +1,26 @@ +package com.placeholder.sherpai2.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.placeholder.sherpai2.data.local.entity.EventEntity + +@Dao +interface EventDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(event: EventEntity) + + /** + * Find events covering a timestamp. + * + * This is the backbone of auto-tagging by date. + */ + @Query(""" + SELECT * FROM events + WHERE :timestamp BETWEEN startDate AND endDate + AND isHidden = 0 + """) + suspend fun findEventsForTimestamp(timestamp: Long): List +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageAggregateDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageAggregateDao.kt new file mode 100644 index 0000000..4f1841a --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageAggregateDao.kt @@ -0,0 +1,25 @@ +package com.placeholder.sherpai2.data.local.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import com.placeholder.sherpai2.data.local.model.ImageWithEverything +import kotlinx.coroutines.flow.Flow + +@Dao +interface ImageAggregateDao { + + /** + * Observe a fully-hydrated image object. + * + * This is what your detail screen should use. + */ + @Transaction + @Query(""" + SELECT * FROM images + WHERE imageId = :imageId + """) + fun observeImageWithEverything( + imageId: String + ): Flow +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageDao.kt new file mode 100644 index 0000000..e85e84c --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageDao.kt @@ -0,0 +1,56 @@ +package com.placeholder.sherpai2.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.placeholder.sherpai2.data.local.entity.ImageEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ImageDao { + + /** + * Insert images. + * + * IGNORE prevents duplicate insertion + * when sha256 or imageUri already exists. + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertImages(images: List) + + /** + * Get image by ID. + */ + @Query("SELECT * FROM images WHERE imageId = :imageId") + suspend fun getImageById(imageId: String): ImageEntity? + + /** + * Stream images ordered by capture time (newest first). + * + * Flow is critical: + * - UI auto-updates + * - No manual refresh + */ + @Query(""" + SELECT * FROM images + ORDER BY capturedAt DESC + """) + fun observeAllImages(): Flow> + + /** + * Fetch images in a time range. + * Used for: + * - event auto-assignment + * - timeline views + */ + @Query(""" + SELECT * FROM images + WHERE capturedAt BETWEEN :start AND :end + ORDER BY capturedAt ASC + """) + suspend fun getImagesInRange( + start: Long, + end: Long + ): List +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageEventDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageEventDao.kt new file mode 100644 index 0000000..bf7f799 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageEventDao.kt @@ -0,0 +1,23 @@ +package com.placeholder.sherpai2.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.placeholder.sherpai2.data.local.entity.ImageEventEntity + +@Dao +interface ImageEventDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(entity: ImageEventEntity) + + /** + * Images associated with an event. + */ + @Query(""" + SELECT imageId FROM image_events + WHERE eventId = :eventId + """) + suspend fun findImagesForEvent(eventId: String): List +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImagePersonDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImagePersonDao.kt new file mode 100644 index 0000000..51e5903 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImagePersonDao.kt @@ -0,0 +1,25 @@ +package com.placeholder.sherpai2.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.placeholder.sherpai2.data.local.entity.ImagePersonEntity + +@Dao +interface ImagePersonDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(entity: ImagePersonEntity) + + /** + * All images containing a specific person. + */ + @Query(""" + SELECT imageId FROM image_persons + WHERE personId = :personId + AND visibility = 'PUBLIC' + AND confirmed = 1 + """) + suspend fun findImagesForPerson(personId: String): List +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageTagDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageTagDao.kt new file mode 100644 index 0000000..20bb08c --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageTagDao.kt @@ -0,0 +1,41 @@ +package com.placeholder.sherpai2.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.placeholder.sherpai2.data.local.entity.ImageTagEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ImageTagDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(imageTag: ImageTagEntity) + + /** + * Observe tags for an image. + */ + @Query(""" + SELECT * FROM image_tags + WHERE imageId = :imageId + AND visibility != 'HIDDEN' + """) + fun observeTagsForImage(imageId: String): Flow> + + /** + * Find images by tag. + * + * This is your primary tag-search query. + */ + @Query(""" + SELECT imageId FROM image_tags + WHERE tagId = :tagId + AND visibility = 'PUBLIC' + AND confidence >= :minConfidence + """) + suspend fun findImagesByTag( + tagId: String, + minConfidence: Float = 0.5f + ): List +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/PersonDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/PersonDao.kt new file mode 100644 index 0000000..4f56365 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/PersonDao.kt @@ -0,0 +1,24 @@ +package com.placeholder.sherpai2.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.placeholder.sherpai2.data.local.entity.PersonEntity + +@Dao +interface PersonDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(person: PersonEntity) + + @Query("SELECT * FROM persons WHERE personId = :personId") + suspend fun getById(personId: String): PersonEntity? + + @Query(""" + SELECT * FROM persons + WHERE isHidden = 0 + ORDER BY displayName + """) + suspend fun getVisiblePeople(): List +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/TagDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/TagDao.kt new file mode 100644 index 0000000..dec14cb --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/TagDao.kt @@ -0,0 +1,24 @@ +package com.placeholder.sherpai2.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.placeholder.sherpai2.data.local.entity.TagEntity + +@Dao +interface TagDao { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(tag: TagEntity) + + /** + * Resolve a tag by value. + * Example: "park" + */ + @Query("SELECT * FROM tags WHERE value = :value LIMIT 1") + suspend fun getByValue(value: String): TagEntity? + + @Query("SELECT * FROM tags") + suspend fun getAll(): List +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/EventEntity.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/EventEntity.kt new file mode 100644 index 0000000..68176b2 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/EventEntity.kt @@ -0,0 +1,44 @@ +package com.placeholder.sherpai2.data.local.entity + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +/** + * Represents a meaningful event spanning a time range. + * + * Events allow auto-association of images by timestamp. + */ +@Entity( + tableName = "events", + indices = [ + Index(value = ["startDate"]), + Index(value = ["endDate"]) + ] +) +data class EventEntity( + + @PrimaryKey + val eventId: String, + + val name: String, + + /** + * Inclusive start date (UTC millis). + */ + val startDate: Long, + + /** + * Inclusive end date (UTC millis). + */ + val endDate: Long, + + val location: String?, + + /** + * 0.0 – 1.0 importance weight + */ + val importance: Float, + + val isHidden: Boolean +) diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageEntity.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageEntity.kt new file mode 100644 index 0000000..0cc7ed7 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageEntity.kt @@ -0,0 +1,55 @@ +package com.placeholder.sherpai2.data.local.entity + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +/** + * Represents a single image on the device. + * + * This entity is intentionally immutable: + * - imageUri identifies where the image lives + * - sha256 prevents duplicates + * - capturedAt is the EXIF timestamp + * + * This table should be append-only. + */ +@Entity( + tableName = "images", + indices = [ + Index(value = ["imageUri"], unique = true), + Index(value = ["sha256"], unique = true), + Index(value = ["capturedAt"]) + ] +) +data class ImageEntity( + + @PrimaryKey + val imageId: String, + + val imageUri: String, + + /** + * Cryptographic hash of image bytes. + * Used for deduplication and re-indexing. + */ + val sha256: String, + + /** + * EXIF timestamp (UTC millis). + */ + val capturedAt: Long, + + /** + * When this image was indexed into the app. + */ + val ingestedAt: Long, + + val width: Int, + val height: Int, + + /** + * CAMERA | SCREENSHOT | IMPORTED + */ + val source: String +) diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageEventEntity.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageEventEntity.kt new file mode 100644 index 0000000..ab56476 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageEventEntity.kt @@ -0,0 +1,42 @@ +package com.placeholder.sherpai2.data.local.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Entity( + tableName = "image_events", + primaryKeys = ["imageId", "eventId"], + foreignKeys = [ + ForeignKey( + entity = ImageEntity::class, + parentColumns = ["imageId"], + childColumns = ["imageId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = EventEntity::class, + parentColumns = ["eventId"], + childColumns = ["eventId"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [ + Index("eventId") + ] +) +data class ImageEventEntity( + + val imageId: String, + val eventId: String, + + /** + * AUTO | MANUAL + */ + val source: String, + + /** + * User override flag. + */ + val override: Boolean +) diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImagePersonEntity.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImagePersonEntity.kt new file mode 100644 index 0000000..e327675 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImagePersonEntity.kt @@ -0,0 +1,40 @@ +package com.placeholder.sherpai2.data.local.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Entity( + tableName = "image_persons", + primaryKeys = ["imageId", "personId"], + foreignKeys = [ + ForeignKey( + entity = ImageEntity::class, + parentColumns = ["imageId"], + childColumns = ["imageId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = PersonEntity::class, + parentColumns = ["personId"], + childColumns = ["personId"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [ + Index("personId") + ] +) +data class ImagePersonEntity( + + val imageId: String, + val personId: String, + + val confidence: Float, + val confirmed: Boolean, + + /** + * PUBLIC | PRIVATE + */ + val visibility: String +) diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageTagEntity.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageTagEntity.kt new file mode 100644 index 0000000..8232b4f --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/ImageTagEntity.kt @@ -0,0 +1,56 @@ +package com.placeholder.sherpai2.data.local.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +/** + * Join table linking images to tags. + * + * This is NOT optional. + * Do not inline tag lists on ImageEntity. + */ +@Entity( + tableName = "image_tags", + primaryKeys = ["imageId", "tagId"], + foreignKeys = [ + ForeignKey( + entity = ImageEntity::class, + parentColumns = ["imageId"], + childColumns = ["imageId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = TagEntity::class, + parentColumns = ["tagId"], + childColumns = ["tagId"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [ + Index("tagId"), + Index("imageId") + ] +) +data class ImageTagEntity( + + val imageId: String, + val tagId: String, + + /** + * AUTO | MANUAL + */ + val source: String, + + /** + * ML confidence (0–1). + */ + val confidence: Float, + + /** + * PUBLIC | PRIVATE | HIDDEN + */ + val visibility: String, + + val createdAt: Long +) diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/PersonsEntity.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/PersonsEntity.kt new file mode 100644 index 0000000..1360a3f --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/PersonsEntity.kt @@ -0,0 +1,30 @@ +package com.placeholder.sherpai2.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Represents a known person. + * + * People are separate from generic tags because: + * - face embeddings + * - privacy rules + * - identity merging + */ +@Entity(tableName = "persons") +data class PersonEntity( + + @PrimaryKey + val personId: String, + + val displayName: String, + + /** + * Reference to face embedding storage (ML layer). + */ + val faceEmbeddingId: String?, + + val isHidden: Boolean, + + val createdAt: Long +) diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/TagEntity.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/TagEntity.kt new file mode 100644 index 0000000..00b7ebc --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/TagEntity.kt @@ -0,0 +1,30 @@ +package com.placeholder.sherpai2.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Represents a conceptual tag. + * + * Tags are normalized so that: + * - "park" exists once + * - many images can reference it + */ +@Entity(tableName = "tags") +data class TagEntity( + + @PrimaryKey + val tagId: String, + + /** + * GENERIC | SYSTEM | HIDDEN + */ + val type: String, + + /** + * Human-readable value, e.g. "park", "sunset" + */ + val value: String, + + val createdAt: Long +) diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/model/ImageWithEverything.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/model/ImageWithEverything.kt new file mode 100644 index 0000000..ecbd352 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/model/ImageWithEverything.kt @@ -0,0 +1,29 @@ +package com.placeholder.sherpai2.data.local.model + +import androidx.room.Embedded +import androidx.room.Relation +import com.placeholder.sherpai2.data.local.entity.* + +data class ImageWithEverything( + + @Embedded + val image: ImageEntity, + + @Relation( + parentColumn = "imageId", + entityColumn = "imageId" + ) + val tags: List, + + @Relation( + parentColumn = "imageId", + entityColumn = "imageId" + ) + val persons: List, + + @Relation( + parentColumn = "imageId", + entityColumn = "imageId" + ) + val events: List +) diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/model/ImageWithTags.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/model/ImageWithTags.kt new file mode 100644 index 0000000..9da3522 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/model/ImageWithTags.kt @@ -0,0 +1,18 @@ +package com.placeholder.sherpai2.data.local.model + +import androidx.room.Embedded +import androidx.room.Relation +import com.placeholder.sherpai2.data.local.entity.ImageEntity +import com.placeholder.sherpai2.data.local.entity.ImageTagEntity + +data class ImageWithTags( + + @Embedded + val image: ImageEntity, + + @Relation( + parentColumn = "imageId", + entityColumn = "imageId" + ) + val tags: List +) diff --git a/app/src/main/java/com/placeholder/sherpai2/di/DatabaseModule.kt b/app/src/main/java/com/placeholder/sherpai2/di/DatabaseModule.kt new file mode 100644 index 0000000..cf813a7 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/di/DatabaseModule.kt @@ -0,0 +1,28 @@ +package com.placeholder.sherpai2.di + +import android.content.Context +import androidx.room.Room +import com.placeholder.sherpai2.data.local.AppDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun provideDatabase( + @ApplicationContext context: Context + ): AppDatabase { + return Room.databaseBuilder( + context, + AppDatabase::class.java, + "sherpai.db" + ).build() + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 952b930..963bc56 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.ksp) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2701157..6d15a2c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,32 +1,62 @@ [versions] -agp = "8.13.1" +# Tooling +agp = "8.7.3" kotlin = "2.0.21" -coreKtx = "1.17.0" -junit = "4.13.2" -junitVersion = "1.3.0" -espressoCore = "3.7.0" -lifecycleRuntimeKtx = "2.10.0" -activityCompose = "1.12.1" -composeBom = "2024.09.00" +ksp = "2.0.21-1.0.28" + +# AndroidX / Lifecycle +coreKtx = "1.15.0" +lifecycle = "2.8.7" +activityCompose = "1.9.3" +composeBom = "2024.12.01" +navigationCompose = "2.8.5" +hiltNavigationCompose = "1.2.0" + +# DI +hilt = "2.52" + +# Images +coil = "2.7.0" + +#backend2 +#Room +room = "2.6.1" + [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } + +# Compose BOM & UI androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } -androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } # ADDED THIS androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } + +# Navigation & Hilt +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } + +# Misc +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } + +#backend2 +#Room +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } - +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 168d13e..f0bd326 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Dec 09 23:28:28 EST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists