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 index 02c0a44..6bda55f 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/local/AppDatabase.kt +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/AppDatabase.kt @@ -28,7 +28,7 @@ import com.placeholder.sherpai2.data.local.entity.* FaceModelEntity::class, // NEW: Face embeddings PhotoFaceTagEntity::class // NEW: Face tags ], - version = 4, + version = 5, exportSchema = false ) // No TypeConverters needed - embeddings stored as strings 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 index f683005..a414a6c 100644 --- 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 @@ -15,9 +15,6 @@ interface ImageTagDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(imageTag: ImageTagEntity) - /** - * Observe tags for an image. - */ @Query(""" SELECT * FROM image_tags WHERE imageId = :imageId @@ -26,9 +23,7 @@ interface ImageTagDao { fun observeTagsForImage(imageId: String): Flow> /** - * Find images by tag. - * - * This is your primary tag-search query. + * FIXED: Removed default parameter */ @Query(""" SELECT imageId FROM image_tags @@ -38,7 +33,7 @@ interface ImageTagDao { """) suspend fun findImagesByTag( tagId: String, - minConfidence: Float = 0.5f + minConfidence: Float ): List @Transaction @@ -49,7 +44,4 @@ interface ImageTagDao { WHERE it.imageId = :imageId AND it.visibility = 'PUBLIC' """) fun getTagsForImage(imageId: String): Flow> - - - -} +} \ No newline at end of file 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 index 87c8c8a..994aada 100644 --- 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 @@ -4,16 +4,11 @@ import androidx.room.* import com.placeholder.sherpai2.data.local.entity.PersonEntity import kotlinx.coroutines.flow.Flow -/** - * PersonDao - Data access for PersonEntity - * - * PRIMARY KEY TYPE: String (UUID) - */ @Dao interface PersonDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(person: PersonEntity): Long // Room still returns row ID as Long + suspend fun insert(person: PersonEntity): Long @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(persons: List) @@ -21,8 +16,11 @@ interface PersonDao { @Update suspend fun update(person: PersonEntity) + /** + * FIXED: Removed default parameter + */ @Query("UPDATE persons SET updatedAt = :timestamp WHERE id = :personId") - suspend fun updateTimestamp(personId: String, timestamp: Long = System.currentTimeMillis()) + suspend fun updateTimestamp(personId: String, timestamp: Long) @Delete suspend fun delete(person: PersonEntity) @@ -50,4 +48,4 @@ interface PersonDao { @Query("SELECT EXISTS(SELECT 1 FROM persons WHERE id = :personId)") suspend fun personExists(personId: String): Boolean -} \ No newline at end of file +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/Photofacetagdao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/Photofacetagdao.kt index d873d68..842e026 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/Photofacetagdao.kt +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/Photofacetagdao.kt @@ -4,17 +4,11 @@ import androidx.room.* import kotlinx.coroutines.flow.Flow import com.placeholder.sherpai2.data.local.entity.PhotoFaceTagEntity -/** - * PhotoFaceTagDao - Manages face tags in photos - * - * PRIMARY KEY TYPE: String (UUID) - * FOREIGN KEYS: imageId (String), faceModelId (String) - */ @Dao interface PhotoFaceTagDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertTag(tag: PhotoFaceTagEntity): Long // Row ID + suspend fun insertTag(tag: PhotoFaceTagEntity): Long @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertTags(tags: List) @@ -22,8 +16,11 @@ interface PhotoFaceTagDao { @Update suspend fun updateTag(tag: PhotoFaceTagEntity) + /** + * FIXED: Removed default parameter + */ @Query("UPDATE photo_face_tags SET verifiedByUser = 1, verifiedAt = :timestamp WHERE id = :tagId") - suspend fun markTagAsVerified(tagId: String, timestamp: Long = System.currentTimeMillis()) + suspend fun markTagAsVerified(tagId: String, timestamp: Long) // ===== QUERY BY IMAGE ===== @@ -66,8 +63,11 @@ interface PhotoFaceTagDao { // ===== STATISTICS ===== + /** + * FIXED: Removed default parameter + */ @Query("SELECT * FROM photo_face_tags WHERE confidence < :threshold ORDER BY confidence ASC") - suspend fun getLowConfidenceTags(threshold: Float = 0.7f): List + suspend fun getLowConfidenceTags(threshold: Float): List @Query("SELECT * FROM photo_face_tags WHERE verifiedByUser = 0 ORDER BY detectedAt DESC") suspend fun getUnverifiedTags(): List @@ -78,14 +78,14 @@ interface PhotoFaceTagDao { @Query("SELECT AVG(confidence) FROM photo_face_tags WHERE faceModelId = :faceModelId") suspend fun getAverageConfidenceForFaceModel(faceModelId: String): Float? + /** + * FIXED: Removed default parameter + */ @Query("SELECT * FROM photo_face_tags ORDER BY detectedAt DESC LIMIT :limit") - suspend fun getRecentlyDetectedFaces(limit: Int = 20): List + suspend fun getRecentlyDetectedFaces(limit: Int): List } -/** - * Simple data class for photo counts - */ data class FaceModelPhotoCount( val faceModelId: String, val photoCount: Int -) \ No newline at end of file +) 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 index f5c8a07..01fe277 100644 --- 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 @@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.Flow /** * TagDao - Tag management with face recognition integration + * + * NO DEFAULT PARAMETERS - Room doesn't support them in @Query methods */ @Dao interface TagDao { @@ -46,6 +48,8 @@ interface TagDao { /** * Get most used tags WITH usage counts + * + * @param limit Maximum number of tags to return */ @Query(""" SELECT t.tagId, t.type, t.value, t.createdAt, @@ -56,7 +60,7 @@ interface TagDao { ORDER BY usage_count DESC LIMIT :limit """) - suspend fun getMostUsedTags(limit: Int = 10): List + suspend fun getMostUsedTags(limit: Int): List /** * Get tag usage count @@ -138,6 +142,8 @@ interface TagDao { /** * Suggest tags based on person's relationship + * + * @param limit Maximum number of suggestions */ @Query(""" SELECT DISTINCT t.* FROM tags t @@ -154,11 +160,13 @@ interface TagDao { suspend fun suggestTagsBasedOnRelationship( relationship: String, excludePersonId: String, - limit: Int = 5 + limit: Int ): List /** * Get tags commonly used with this tag + * + * @param limit Maximum number of related tags */ @Query(""" SELECT DISTINCT t2.* FROM tags t2 @@ -174,7 +182,7 @@ interface TagDao { """) suspend fun getRelatedTags( tagId: String, - limit: Int = 5 + limit: Int ): List // ====================== @@ -183,6 +191,8 @@ interface TagDao { /** * Search tags by value (partial match) + * + * @param limit Maximum number of results */ @Query(""" SELECT * FROM tags @@ -190,10 +200,12 @@ interface TagDao { ORDER BY value ASC LIMIT :limit """) - suspend fun searchTags(query: String, limit: Int = 20): List + suspend fun searchTags(query: String, limit: Int): List /** * Search tags with usage count + * + * @param limit Maximum number of results */ @Query(""" SELECT t.tagId, t.type, t.value, t.createdAt, @@ -205,5 +217,5 @@ interface TagDao { ORDER BY usage_count DESC, t.value ASC LIMIT :limit """) - suspend fun searchTagsWithUsage(query: String, limit: Int = 20): List + suspend fun searchTagsWithUsage(query: String, limit: Int): List } \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/Facerecognitionentities.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/Facerecognitionentities.kt index a917b59..0bddef8 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/Facerecognitionentities.kt +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/Facerecognitionentities.kt @@ -1,5 +1,6 @@ package com.placeholder.sherpai2.data.local.entity +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index @@ -7,90 +8,57 @@ import androidx.room.PrimaryKey import java.util.UUID /** - * PersonEntity - Represents a person in the face recognition system - * - * TABLE: persons - * PRIMARY KEY: id (String) + * PersonEntity - NO DEFAULT VALUES for KSP compatibility */ @Entity( tableName = "persons", - indices = [ - Index(value = ["name"]) - ] + indices = [Index(value = ["name"])] ) -/** -* PersonEntity - Represents a person in your app -* -* CLEAN DESIGN: -* - Uses String UUID for id (matches your ImageEntity.imageId pattern) -* - Face embeddings stored separately in FaceModelEntity -* - Simple, extensible schema -* - Now includes DOB and relationship for better organization -*/ data class PersonEntity( @PrimaryKey - val id: String = UUID.randomUUID().toString(), + @ColumnInfo(name = "id") + val id: String, // โ† No default - /** - * Person's name (required) - */ + @ColumnInfo(name = "name") val name: String, - /** - * Date of birth (optional) - * Stored as Unix timestamp (milliseconds) - */ - val dateOfBirth: Long? = null, + @ColumnInfo(name = "dateOfBirth") + val dateOfBirth: Long?, - /** - * Relationship to user (optional) - * Examples: "Family", "Friend", "Partner", "Child", "Parent", "Sibling", "Colleague", "Other" - */ - val relationship: String? = null, + @ColumnInfo(name = "relationship") + val relationship: String?, - /** - * When this person was added - */ - val createdAt: Long = System.currentTimeMillis(), + @ColumnInfo(name = "createdAt") + val createdAt: Long, // โ† No default - /** - * Last time this person's data was updated - */ - val updatedAt: Long = System.currentTimeMillis() + @ColumnInfo(name = "updatedAt") + val updatedAt: Long // โ† No default ) { companion object { - /** - * Create PersonEntity with optional fields - */ fun create( name: String, dateOfBirth: Long? = null, relationship: String? = null ): PersonEntity { + val now = System.currentTimeMillis() return PersonEntity( + id = UUID.randomUUID().toString(), name = name, dateOfBirth = dateOfBirth, - relationship = relationship + relationship = relationship, + createdAt = now, + updatedAt = now ) } } - /** - * Calculate age if date of birth is available - */ fun getAge(): Int? { if (dateOfBirth == null) return null - val now = System.currentTimeMillis() val ageInMillis = now - dateOfBirth - val ageInYears = ageInMillis / (1000L * 60 * 60 * 24 * 365) - - return ageInYears.toInt() + return (ageInMillis / (1000L * 60 * 60 * 24 * 365)).toInt() } - /** - * Get relationship emoji - */ fun getRelationshipEmoji(): String { return when (relationship) { "Family" -> "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ" @@ -104,11 +72,9 @@ data class PersonEntity( } } } + /** - * FaceModelEntity - Stores face recognition model (embedding) for a person - * - * TABLE: face_models - * FOREIGN KEY: personId โ†’ persons.id + * FaceModelEntity - NO DEFAULT VALUES */ @Entity( tableName = "face_models", @@ -120,22 +86,36 @@ data class PersonEntity( onDelete = ForeignKey.CASCADE ) ], - indices = [ - Index(value = ["personId"], unique = true) - ] + indices = [Index(value = ["personId"], unique = true)] ) data class FaceModelEntity( @PrimaryKey - val id: String = UUID.randomUUID().toString(), + @ColumnInfo(name = "id") + val id: String, // โ† No default + @ColumnInfo(name = "personId") val personId: String, - val embedding: String, // Serialized FloatArray + + @ColumnInfo(name = "embedding") + val embedding: String, + + @ColumnInfo(name = "trainingImageCount") val trainingImageCount: Int, + + @ColumnInfo(name = "averageConfidence") val averageConfidence: Float, - val createdAt: Long = System.currentTimeMillis(), - val updatedAt: Long = System.currentTimeMillis(), - val lastUsed: Long? = null, - val isActive: Boolean = true + + @ColumnInfo(name = "createdAt") + val createdAt: Long, // โ† No default + + @ColumnInfo(name = "updatedAt") + val updatedAt: Long, // โ† No default + + @ColumnInfo(name = "lastUsed") + val lastUsed: Long?, + + @ColumnInfo(name = "isActive") + val isActive: Boolean ) { companion object { fun create( @@ -144,11 +124,17 @@ data class FaceModelEntity( trainingImageCount: Int, averageConfidence: Float ): FaceModelEntity { + val now = System.currentTimeMillis() return FaceModelEntity( + id = UUID.randomUUID().toString(), personId = personId, embedding = embeddingArray.joinToString(","), trainingImageCount = trainingImageCount, - averageConfidence = averageConfidence + averageConfidence = averageConfidence, + createdAt = now, + updatedAt = now, + lastUsed = null, + isActive = true ) } } @@ -159,12 +145,7 @@ data class FaceModelEntity( } /** - * PhotoFaceTagEntity - Links detected faces in photos to person models - * - * TABLE: photo_face_tags - * FOREIGN KEYS: - * - imageId โ†’ images.imageId (String) - * - faceModelId โ†’ face_models.id (String) + * PhotoFaceTagEntity - NO DEFAULT VALUES */ @Entity( tableName = "photo_face_tags", @@ -190,18 +171,32 @@ data class FaceModelEntity( ) data class PhotoFaceTagEntity( @PrimaryKey - val id: String = UUID.randomUUID().toString(), + @ColumnInfo(name = "id") + val id: String, // โ† No default - val imageId: String, // String to match ImageEntity.imageId + @ColumnInfo(name = "imageId") + val imageId: String, + + @ColumnInfo(name = "faceModelId") val faceModelId: String, - val boundingBox: String, // "left,top,right,bottom" - val confidence: Float, - val embedding: String, // Serialized FloatArray + @ColumnInfo(name = "boundingBox") + val boundingBox: String, - val detectedAt: Long = System.currentTimeMillis(), - val verifiedByUser: Boolean = false, - val verifiedAt: Long? = null + @ColumnInfo(name = "confidence") + val confidence: Float, + + @ColumnInfo(name = "embedding") + val embedding: String, + + @ColumnInfo(name = "detectedAt") + val detectedAt: Long, // โ† No default + + @ColumnInfo(name = "verifiedByUser") + val verifiedByUser: Boolean, + + @ColumnInfo(name = "verifiedAt") + val verifiedAt: Long? ) { companion object { fun create( @@ -212,11 +207,15 @@ data class PhotoFaceTagEntity( faceEmbedding: FloatArray ): PhotoFaceTagEntity { return PhotoFaceTagEntity( + id = UUID.randomUUID().toString(), imageId = imageId, faceModelId = faceModelId, boundingBox = "${boundingBox.left},${boundingBox.top},${boundingBox.right},${boundingBox.bottom}", confidence = confidence, - embedding = faceEmbedding.joinToString(",") + embedding = faceEmbedding.joinToString(","), + detectedAt = System.currentTimeMillis(), + verifiedByUser = false, + verifiedAt = null ) } } diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/PersonEntity b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/PersonEntity index dca01dd..e69de29 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/local/entity/PersonEntity +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/entity/PersonEntity @@ -1,49 +0,0 @@ -package com.placeholder.sherpai2.data.local.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey - -/** - * PersonEntity - Represents a person in your app - * - * This is a SIMPLE person entity for your existing database. - * Face embeddings are stored separately in FaceModelEntity. - * - * ARCHITECTURE: - * - PersonEntity = Human data (name, birthday, etc.) - * - FaceModelEntity = AI data (face embeddings) - links to this via personId - * - * You can add more fields as needed: - * - birthday: Long? - * - phoneNumber: String? - * - email: String? - * - notes: String? - * - etc. - */ -@Entity(tableName = "persons") -data class PersonEntity( - @PrimaryKey(autoGenerate = true) - val id: Long = 0, - - /** - * Person's name - */ - val name: String, - - /** - * When this person was added - */ - val createdAt: Long = System.currentTimeMillis(), - - /** - * Last time this person's data was updated - */ - val updatedAt: Long = System.currentTimeMillis() - - // ADD MORE FIELDS AS NEEDED: - // val birthday: Long? = null, - // val phoneNumber: String? = null, - // val email: String? = null, - // val profilePhotoUri: String? = null, - // val notes: String? = null -) \ No newline at end of file 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 index fde24f0..04d542e 100644 --- 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 @@ -5,68 +5,113 @@ import androidx.room.Entity import androidx.room.PrimaryKey import java.util.UUID +/** + * Tag type constants - MUST be defined BEFORE TagEntity + * to avoid KSP initialization order issues + */ +object TagType { + const val GENERIC = "GENERIC" // User tags + const val SYSTEM = "SYSTEM" // AI/auto tags + const val HIDDEN = "HIDDEN" // Internal +} + +/** + * Common system tag values + */ +object SystemTags { + const val HAS_FACES = "has_faces" + const val MULTIPLE_PEOPLE = "multiple_people" + const val LANDSCAPE = "landscape" + const val PORTRAIT = "portrait" + const val LOW_QUALITY = "low_quality" + const val BLURRY = "blurry" +} + /** * TagEntity - Normalized tag storage * - * DESIGN: - * - Tags exist once (e.g., "vacation") - * - Multiple images reference via ImageTagEntity junction table - * - Type system: GENERIC | SYSTEM | HIDDEN + * EXPLICIT COLUMN MAPPINGS for KSP compatibility */ @Entity(tableName = "tags") data class TagEntity( @PrimaryKey - val tagId: String = UUID.randomUUID().toString(), + @ColumnInfo(name = "tagId") + val tagId: String, - /** - * Tag type: GENERIC | SYSTEM | HIDDEN - */ - val type: String = TagType.GENERIC, + @ColumnInfo(name = "type") + val type: String, - /** - * Human-readable value, e.g. "vacation", "beach" - */ + @ColumnInfo(name = "value") val value: String, - /** - * When tag was created - */ - val createdAt: Long = System.currentTimeMillis() + @ColumnInfo(name = "createdAt") + val createdAt: Long ) { companion object { + /** + * Create a generic user tag + */ fun createUserTag(value: String): TagEntity { return TagEntity( + tagId = UUID.randomUUID().toString(), type = TagType.GENERIC, - value = value.trim().lowercase() + value = value.trim().lowercase(), + createdAt = System.currentTimeMillis() ) } + /** + * Create a system tag (auto-generated) + */ fun createSystemTag(value: String): TagEntity { return TagEntity( + tagId = UUID.randomUUID().toString(), type = TagType.SYSTEM, - value = value.trim().lowercase() + value = value.trim().lowercase(), + createdAt = System.currentTimeMillis() ) } + /** + * Create hidden tag (internal use) + */ fun createHiddenTag(value: String): TagEntity { return TagEntity( + tagId = UUID.randomUUID().toString(), type = TagType.HIDDEN, - value = value.trim().lowercase() + value = value.trim().lowercase(), + createdAt = System.currentTimeMillis() ) } } + /** + * Check if this is a user-created tag + */ fun isUserTag(): Boolean = type == TagType.GENERIC + + /** + * Check if this is a system tag + */ fun isSystemTag(): Boolean = type == TagType.SYSTEM + + /** + * Check if this is a hidden tag + */ fun isHiddenTag(): Boolean = type == TagType.HIDDEN + + /** + * Get display value (capitalized for UI) + */ fun getDisplayValue(): String = value.replaceFirstChar { it.uppercase() } } /** * TagWithUsage - For queries that include usage count * - * Use this for statistics queries + * NOT AN ENTITY - just a POJO for query results + * Do NOT add this to @Database entities list! */ data class TagWithUsage( @ColumnInfo(name = "tagId") @@ -95,25 +140,4 @@ data class TagWithUsage( createdAt = createdAt ) } -} - -/** - * Tag type constants - */ -object TagType { - const val GENERIC = "GENERIC" - const val SYSTEM = "SYSTEM" - const val HIDDEN = "HIDDEN" -} - -/** - * Common system tag values - */ -object SystemTags { - const val HAS_FACES = "has_faces" - const val MULTIPLE_PEOPLE = "multiple_people" - const val LANDSCAPE = "landscape" - const val PORTRAIT = "portrait" - const val LOW_QUALITY = "low_quality" - const val BLURRY = "blurry" } \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/data/repository/Facerecognitionrepository.kt b/app/src/main/java/com/placeholder/sherpai2/data/repository/Facerecognitionrepository.kt index 02123ff..368cc2e 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/repository/Facerecognitionrepository.kt +++ b/app/src/main/java/com/placeholder/sherpai2/data/repository/Facerecognitionrepository.kt @@ -52,7 +52,8 @@ class FaceRecognitionRepository @Inject constructor( ): String = withContext(Dispatchers.IO) { // Create PersonEntity with UUID - val person = PersonEntity(name = personName) + val person = PersonEntity.create(name = personName) + personDao.insert(person) // Train face model @@ -312,7 +313,10 @@ class FaceRecognitionRepository @Inject constructor( // ====================== suspend fun verifyFaceTag(tagId: String) { - photoFaceTagDao.markTagAsVerified(tagId) + photoFaceTagDao.markTagAsVerified( + tagId = tagId, + timestamp = System.currentTimeMillis() + ) } suspend fun getUnverifiedTags(): 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 index 90acb2e..ea9399a 100644 --- a/app/src/main/java/com/placeholder/sherpai2/di/DatabaseModule.kt +++ b/app/src/main/java/com/placeholder/sherpai2/di/DatabaseModule.kt @@ -12,97 +12,68 @@ import dagger.hilt.components.SingletonComponent import javax.inject.Singleton /** - * DatabaseModule - Provides database and DAOs + * DatabaseModule - Provides database and ALL DAOs * - * FRESH START VERSION: - * - No migration needed - * - Uses fallbackToDestructiveMigration (deletes old database) - * - Perfect for development + * DEVELOPMENT CONFIGURATION: + * - fallbackToDestructiveMigration enabled + * - No migrations required */ @Module @InstallIn(SingletonComponent::class) object DatabaseModule { + // ===== DATABASE ===== + @Provides @Singleton fun provideDatabase( @ApplicationContext context: Context - ): AppDatabase { - return Room.databaseBuilder( + ): AppDatabase = + Room.databaseBuilder( context, AppDatabase::class.java, "sherpai.db" ) - .fallbackToDestructiveMigration() // โ† Deletes old database, creates fresh + .fallbackToDestructiveMigration() .build() - } - // ===== YOUR EXISTING DAOs ===== + // ===== CORE DAOs ===== @Provides - fun provideImageDao(database: AppDatabase): ImageDao { - return database.imageDao() - } + fun provideImageDao(db: AppDatabase): ImageDao = + db.imageDao() @Provides - fun provideTagDao(database: AppDatabase): TagDao { - return database.tagDao() - } + fun provideTagDao(db: AppDatabase): TagDao = + db.tagDao() @Provides - fun provideEventDao(database: AppDatabase): EventDao { - return database.eventDao() - } + fun provideEventDao(db: AppDatabase): EventDao = + db.eventDao() @Provides - fun provideImageTagDao(database: AppDatabase): ImageTagDao { - return database.imageTagDao() - } + fun provideImageEventDao(db: AppDatabase): ImageEventDao = + db.imageEventDao() @Provides - fun provideImagePersonDao(database: AppDatabase): ImagePersonDao { - return database.imagePersonDao() - } + fun provideImageAggregateDao(db: AppDatabase): ImageAggregateDao = + db.imageAggregateDao() @Provides - fun provideImageEventDao(database: AppDatabase): ImageEventDao { - return database.imageEventDao() - } + fun provideImageTagDao(db: AppDatabase): ImageTagDao = + db.imageTagDao() + + // ===== FACE RECOGNITION DAOs ===== @Provides - fun provideImageAggregateDao(database: AppDatabase): ImageAggregateDao { - return database.imageAggregateDao() - } - - // ===== NEW FACE RECOGNITION DAOs ===== + fun providePersonDao(db: AppDatabase): PersonDao = + db.personDao() @Provides - fun providePersonDao(database: AppDatabase): PersonDao { - return database.personDao() - } + fun provideFaceModelDao(db: AppDatabase): FaceModelDao = + db.faceModelDao() @Provides - fun provideFaceModelDao(database: AppDatabase): FaceModelDao { - return database.faceModelDao() - } - - @Provides - fun providePhotoFaceTagDao(database: AppDatabase): PhotoFaceTagDao { - return database.photoFaceTagDao() - } + fun providePhotoFaceTagDao(db: AppDatabase): PhotoFaceTagDao = + db.photoFaceTagDao() } - -/** - * NOTES: - * - * fallbackToDestructiveMigration(): - * - Deletes database if schema changes - * - Creates fresh database with new schema - * - Perfect for development - * - โš ๏ธ Users lose data on updates - * - * For production later: - * - Remove fallbackToDestructiveMigration() - * - Add .addMigrations(MIGRATION_1_2, MIGRATION_2_3, ...) - * - This preserves user data - */ \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainScreen.kt index 051005f..21acc2d 100644 --- a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainScreen.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainScreen.kt @@ -1,32 +1,37 @@ package com.placeholder.sherpai2.ui.presentation +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.navigation.NavController +import androidx.compose.ui.text.font.FontWeight import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.placeholder.sherpai2.ui.navigation.AppNavHost import com.placeholder.sherpai2.ui.navigation.AppRoutes import kotlinx.coroutines.launch +/** + * Beautiful main screen with gradient header, dynamic actions, and polish + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen() { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() - - // Navigation controller for NavHost val navController = rememberNavController() - // Track current backstack entry to update top bar title dynamically val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route ?: AppRoutes.SEARCH - // Drawer content for navigation ModalNavigationDrawer( drawerState = drawerState, drawerContent = { @@ -37,7 +42,6 @@ fun MainScreen() { drawerState.close() if (route != currentRoute) { navController.navigate(route) { - // Avoid multiple copies of the same destination launchSingleTop = true } } @@ -46,17 +50,120 @@ fun MainScreen() { ) }, ) { - // Main scaffold with top bar Scaffold( topBar = { TopAppBar( - title = { Text(currentRoute.replaceFirstChar { it.uppercase() }) }, + title = { + Column { + Text( + text = getScreenTitle(currentRoute), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + getScreenSubtitle(currentRoute)?.let { subtitle -> + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + }, navigationIcon = { - IconButton(onClick = { scope.launch { drawerState.open() } }) { - Icon(Icons.Filled.Menu, contentDescription = "Open Drawer") + IconButton( + onClick = { scope.launch { drawerState.open() } } + ) { + Icon( + Icons.Default.Menu, + contentDescription = "Open Menu", + tint = MaterialTheme.colorScheme.primary + ) + } + }, + actions = { + // Dynamic actions based on current screen + when (currentRoute) { + AppRoutes.SEARCH -> { + IconButton(onClick = { /* TODO: Open filter dialog */ }) { + Icon( + Icons.Default.FilterList, + contentDescription = "Filter", + tint = MaterialTheme.colorScheme.primary + ) + } + } + AppRoutes.INVENTORY -> { + IconButton(onClick = { + navController.navigate(AppRoutes.TRAIN) + }) { + Icon( + Icons.Default.PersonAdd, + contentDescription = "Add Person", + tint = MaterialTheme.colorScheme.primary + ) + } + } + AppRoutes.TAGS -> { + IconButton(onClick = { /* TODO: Add tag */ }) { + Icon( + Icons.Default.Add, + contentDescription = "Add Tag", + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + navigationIconContentColor = MaterialTheme.colorScheme.primary, + actionIconContentColor = MaterialTheme.colorScheme.primary + ) + ) + }, + floatingActionButton = { + // Dynamic FAB based on screen + AnimatedVisibility( + visible = shouldShowFab(currentRoute), + enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() + ) { + when (currentRoute) { + AppRoutes.SEARCH -> { + ExtendedFloatingActionButton( + onClick = { /* TODO: Advanced search */ }, + icon = { + Icon(Icons.Default.Tune, "Advanced Search") + }, + text = { Text("Filters") }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + AppRoutes.TAGS -> { + FloatingActionButton( + onClick = { /* TODO: Add new tag */ }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) { + Icon(Icons.Default.Add, "Add Tag") + } + } + AppRoutes.UPLOAD -> { + ExtendedFloatingActionButton( + onClick = { /* TODO: Select photos */ }, + icon = { Icon(Icons.Default.CloudUpload, "Upload") }, + text = { Text("Select Photos") }, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + } + else -> { + // No FAB for other screens } } - ) + } } ) { paddingValues -> AppNavHost( @@ -66,3 +173,47 @@ fun MainScreen() { } } } + +/** + * Get human-readable screen title + */ +private fun getScreenTitle(route: String): String { + return when (route) { + AppRoutes.SEARCH -> "Search" + AppRoutes.TOUR -> "Explore" // Will be renamed to EXPLORE + AppRoutes.INVENTORY -> "People" + AppRoutes.TRAIN -> "Train New Person" + AppRoutes.MODELS -> "AI Models" + AppRoutes.TAGS -> "Tag Management" + AppRoutes.UPLOAD -> "Upload Photos" + AppRoutes.SETTINGS -> "Settings" + else -> "SherpAI" + } +} + +/** + * Get subtitle for screens that need context + */ +private fun getScreenSubtitle(route: String): String? { + return when (route) { + AppRoutes.SEARCH -> "Find photos by tags, people, or date" + AppRoutes.TOUR -> "Browse your collection" + AppRoutes.INVENTORY -> "Trained face models" + AppRoutes.TRAIN -> "Add a new person to recognize" + AppRoutes.TAGS -> "Organize your photo collection" + AppRoutes.UPLOAD -> "Add photos to your library" + else -> null + } +} + +/** + * Determine if FAB should be shown for current screen + */ +private fun shouldShowFab(route: String): Boolean { + return when (route) { + AppRoutes.SEARCH, + AppRoutes.TAGS, + AppRoutes.UPLOAD -> true + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerScreen.kt index fc1a53b..c4dc4bb 100644 --- a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerScreen.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerScreen.kt @@ -41,6 +41,7 @@ fun FacePickerScreen( // from the Bitmap scale to the UI View scale. Canvas(modifier = Modifier.fillMaxSize().clickable { /* Handle general tap */ }) { // Implementation of coordinate mapping goes here + // TODO implement coordinate mapping } // Simplified: Just show the options as a list of crops if Canvas mapping is too complex for now diff --git a/build.gradle.kts b/build.gradle.kts index a5abe0b..9fcabd9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,4 +8,5 @@ plugins { //https://github.com/google/dagger/issues/4048#issuecomment-1864237679 alias(libs.plugins.ksp) apply false alias(libs.plugins.hilt.android) apply false + } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 20e2a01..374b3eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +org.gradle.java.home=/snap/android-studio/current/jbr