Oh yes - Thats how we do
No default params for KSP complainer fuck UI sweeps
This commit is contained in:
@@ -28,7 +28,7 @@ import com.placeholder.sherpai2.data.local.entity.*
|
|||||||
FaceModelEntity::class, // NEW: Face embeddings
|
FaceModelEntity::class, // NEW: Face embeddings
|
||||||
PhotoFaceTagEntity::class // NEW: Face tags
|
PhotoFaceTagEntity::class // NEW: Face tags
|
||||||
],
|
],
|
||||||
version = 4,
|
version = 5,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
// No TypeConverters needed - embeddings stored as strings
|
// No TypeConverters needed - embeddings stored as strings
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ interface ImageTagDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun upsert(imageTag: ImageTagEntity)
|
suspend fun upsert(imageTag: ImageTagEntity)
|
||||||
|
|
||||||
/**
|
|
||||||
* Observe tags for an image.
|
|
||||||
*/
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT * FROM image_tags
|
SELECT * FROM image_tags
|
||||||
WHERE imageId = :imageId
|
WHERE imageId = :imageId
|
||||||
@@ -26,9 +23,7 @@ interface ImageTagDao {
|
|||||||
fun observeTagsForImage(imageId: String): Flow<List<ImageTagEntity>>
|
fun observeTagsForImage(imageId: String): Flow<List<ImageTagEntity>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find images by tag.
|
* FIXED: Removed default parameter
|
||||||
*
|
|
||||||
* This is your primary tag-search query.
|
|
||||||
*/
|
*/
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT imageId FROM image_tags
|
SELECT imageId FROM image_tags
|
||||||
@@ -38,7 +33,7 @@ interface ImageTagDao {
|
|||||||
""")
|
""")
|
||||||
suspend fun findImagesByTag(
|
suspend fun findImagesByTag(
|
||||||
tagId: String,
|
tagId: String,
|
||||||
minConfidence: Float = 0.5f
|
minConfidence: Float
|
||||||
): List<String>
|
): List<String>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@@ -49,7 +44,4 @@ interface ImageTagDao {
|
|||||||
WHERE it.imageId = :imageId AND it.visibility = 'PUBLIC'
|
WHERE it.imageId = :imageId AND it.visibility = 'PUBLIC'
|
||||||
""")
|
""")
|
||||||
fun getTagsForImage(imageId: String): Flow<List<TagEntity>>
|
fun getTagsForImage(imageId: String): Flow<List<TagEntity>>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,16 +4,11 @@ import androidx.room.*
|
|||||||
import com.placeholder.sherpai2.data.local.entity.PersonEntity
|
import com.placeholder.sherpai2.data.local.entity.PersonEntity
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
/**
|
|
||||||
* PersonDao - Data access for PersonEntity
|
|
||||||
*
|
|
||||||
* PRIMARY KEY TYPE: String (UUID)
|
|
||||||
*/
|
|
||||||
@Dao
|
@Dao
|
||||||
interface PersonDao {
|
interface PersonDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@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)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertAll(persons: List<PersonEntity>)
|
suspend fun insertAll(persons: List<PersonEntity>)
|
||||||
@@ -21,8 +16,11 @@ interface PersonDao {
|
|||||||
@Update
|
@Update
|
||||||
suspend fun update(person: PersonEntity)
|
suspend fun update(person: PersonEntity)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXED: Removed default parameter
|
||||||
|
*/
|
||||||
@Query("UPDATE persons SET updatedAt = :timestamp WHERE id = :personId")
|
@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
|
@Delete
|
||||||
suspend fun delete(person: PersonEntity)
|
suspend fun delete(person: PersonEntity)
|
||||||
|
|||||||
@@ -4,17 +4,11 @@ import androidx.room.*
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import com.placeholder.sherpai2.data.local.entity.PhotoFaceTagEntity
|
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
|
@Dao
|
||||||
interface PhotoFaceTagDao {
|
interface PhotoFaceTagDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertTag(tag: PhotoFaceTagEntity): Long // Row ID
|
suspend fun insertTag(tag: PhotoFaceTagEntity): Long
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertTags(tags: List<PhotoFaceTagEntity>)
|
suspend fun insertTags(tags: List<PhotoFaceTagEntity>)
|
||||||
@@ -22,8 +16,11 @@ interface PhotoFaceTagDao {
|
|||||||
@Update
|
@Update
|
||||||
suspend fun updateTag(tag: PhotoFaceTagEntity)
|
suspend fun updateTag(tag: PhotoFaceTagEntity)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXED: Removed default parameter
|
||||||
|
*/
|
||||||
@Query("UPDATE photo_face_tags SET verifiedByUser = 1, verifiedAt = :timestamp WHERE id = :tagId")
|
@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 =====
|
// ===== QUERY BY IMAGE =====
|
||||||
|
|
||||||
@@ -66,8 +63,11 @@ interface PhotoFaceTagDao {
|
|||||||
|
|
||||||
// ===== STATISTICS =====
|
// ===== STATISTICS =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXED: Removed default parameter
|
||||||
|
*/
|
||||||
@Query("SELECT * FROM photo_face_tags WHERE confidence < :threshold ORDER BY confidence ASC")
|
@Query("SELECT * FROM photo_face_tags WHERE confidence < :threshold ORDER BY confidence ASC")
|
||||||
suspend fun getLowConfidenceTags(threshold: Float = 0.7f): List<PhotoFaceTagEntity>
|
suspend fun getLowConfidenceTags(threshold: Float): List<PhotoFaceTagEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM photo_face_tags WHERE verifiedByUser = 0 ORDER BY detectedAt DESC")
|
@Query("SELECT * FROM photo_face_tags WHERE verifiedByUser = 0 ORDER BY detectedAt DESC")
|
||||||
suspend fun getUnverifiedTags(): List<PhotoFaceTagEntity>
|
suspend fun getUnverifiedTags(): List<PhotoFaceTagEntity>
|
||||||
@@ -78,13 +78,13 @@ interface PhotoFaceTagDao {
|
|||||||
@Query("SELECT AVG(confidence) FROM photo_face_tags WHERE faceModelId = :faceModelId")
|
@Query("SELECT AVG(confidence) FROM photo_face_tags WHERE faceModelId = :faceModelId")
|
||||||
suspend fun getAverageConfidenceForFaceModel(faceModelId: String): Float?
|
suspend fun getAverageConfidenceForFaceModel(faceModelId: String): Float?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXED: Removed default parameter
|
||||||
|
*/
|
||||||
@Query("SELECT * FROM photo_face_tags ORDER BY detectedAt DESC LIMIT :limit")
|
@Query("SELECT * FROM photo_face_tags ORDER BY detectedAt DESC LIMIT :limit")
|
||||||
suspend fun getRecentlyDetectedFaces(limit: Int = 20): List<PhotoFaceTagEntity>
|
suspend fun getRecentlyDetectedFaces(limit: Int): List<PhotoFaceTagEntity>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple data class for photo counts
|
|
||||||
*/
|
|
||||||
data class FaceModelPhotoCount(
|
data class FaceModelPhotoCount(
|
||||||
val faceModelId: String,
|
val faceModelId: String,
|
||||||
val photoCount: Int
|
val photoCount: Int
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TagDao - Tag management with face recognition integration
|
* TagDao - Tag management with face recognition integration
|
||||||
|
*
|
||||||
|
* NO DEFAULT PARAMETERS - Room doesn't support them in @Query methods
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface TagDao {
|
interface TagDao {
|
||||||
@@ -46,6 +48,8 @@ interface TagDao {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get most used tags WITH usage counts
|
* Get most used tags WITH usage counts
|
||||||
|
*
|
||||||
|
* @param limit Maximum number of tags to return
|
||||||
*/
|
*/
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT t.tagId, t.type, t.value, t.createdAt,
|
SELECT t.tagId, t.type, t.value, t.createdAt,
|
||||||
@@ -56,7 +60,7 @@ interface TagDao {
|
|||||||
ORDER BY usage_count DESC
|
ORDER BY usage_count DESC
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
""")
|
""")
|
||||||
suspend fun getMostUsedTags(limit: Int = 10): List<TagWithUsage>
|
suspend fun getMostUsedTags(limit: Int): List<TagWithUsage>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tag usage count
|
* Get tag usage count
|
||||||
@@ -138,6 +142,8 @@ interface TagDao {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Suggest tags based on person's relationship
|
* Suggest tags based on person's relationship
|
||||||
|
*
|
||||||
|
* @param limit Maximum number of suggestions
|
||||||
*/
|
*/
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT DISTINCT t.* FROM tags t
|
SELECT DISTINCT t.* FROM tags t
|
||||||
@@ -154,11 +160,13 @@ interface TagDao {
|
|||||||
suspend fun suggestTagsBasedOnRelationship(
|
suspend fun suggestTagsBasedOnRelationship(
|
||||||
relationship: String,
|
relationship: String,
|
||||||
excludePersonId: String,
|
excludePersonId: String,
|
||||||
limit: Int = 5
|
limit: Int
|
||||||
): List<TagEntity>
|
): List<TagEntity>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tags commonly used with this tag
|
* Get tags commonly used with this tag
|
||||||
|
*
|
||||||
|
* @param limit Maximum number of related tags
|
||||||
*/
|
*/
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT DISTINCT t2.* FROM tags t2
|
SELECT DISTINCT t2.* FROM tags t2
|
||||||
@@ -174,7 +182,7 @@ interface TagDao {
|
|||||||
""")
|
""")
|
||||||
suspend fun getRelatedTags(
|
suspend fun getRelatedTags(
|
||||||
tagId: String,
|
tagId: String,
|
||||||
limit: Int = 5
|
limit: Int
|
||||||
): List<TagEntity>
|
): List<TagEntity>
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
@@ -183,6 +191,8 @@ interface TagDao {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search tags by value (partial match)
|
* Search tags by value (partial match)
|
||||||
|
*
|
||||||
|
* @param limit Maximum number of results
|
||||||
*/
|
*/
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT * FROM tags
|
SELECT * FROM tags
|
||||||
@@ -190,10 +200,12 @@ interface TagDao {
|
|||||||
ORDER BY value ASC
|
ORDER BY value ASC
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
""")
|
""")
|
||||||
suspend fun searchTags(query: String, limit: Int = 20): List<TagEntity>
|
suspend fun searchTags(query: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search tags with usage count
|
* Search tags with usage count
|
||||||
|
*
|
||||||
|
* @param limit Maximum number of results
|
||||||
*/
|
*/
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT t.tagId, t.type, t.value, t.createdAt,
|
SELECT t.tagId, t.type, t.value, t.createdAt,
|
||||||
@@ -205,5 +217,5 @@ interface TagDao {
|
|||||||
ORDER BY usage_count DESC, t.value ASC
|
ORDER BY usage_count DESC, t.value ASC
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
""")
|
""")
|
||||||
suspend fun searchTagsWithUsage(query: String, limit: Int = 20): List<TagWithUsage>
|
suspend fun searchTagsWithUsage(query: String, limit: Int): List<TagWithUsage>
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.placeholder.sherpai2.data.local.entity
|
package com.placeholder.sherpai2.data.local.entity
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
@@ -7,90 +8,57 @@ import androidx.room.PrimaryKey
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PersonEntity - Represents a person in the face recognition system
|
* PersonEntity - NO DEFAULT VALUES for KSP compatibility
|
||||||
*
|
|
||||||
* TABLE: persons
|
|
||||||
* PRIMARY KEY: id (String)
|
|
||||||
*/
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "persons",
|
tableName = "persons",
|
||||||
indices = [
|
indices = [Index(value = ["name"])]
|
||||||
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(
|
data class PersonEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: String = UUID.randomUUID().toString(),
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String, // ← No default
|
||||||
|
|
||||||
/**
|
@ColumnInfo(name = "name")
|
||||||
* Person's name (required)
|
|
||||||
*/
|
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
||||||
/**
|
@ColumnInfo(name = "dateOfBirth")
|
||||||
* Date of birth (optional)
|
val dateOfBirth: Long?,
|
||||||
* Stored as Unix timestamp (milliseconds)
|
|
||||||
*/
|
|
||||||
val dateOfBirth: Long? = null,
|
|
||||||
|
|
||||||
/**
|
@ColumnInfo(name = "relationship")
|
||||||
* Relationship to user (optional)
|
val relationship: String?,
|
||||||
* Examples: "Family", "Friend", "Partner", "Child", "Parent", "Sibling", "Colleague", "Other"
|
|
||||||
*/
|
|
||||||
val relationship: String? = null,
|
|
||||||
|
|
||||||
/**
|
@ColumnInfo(name = "createdAt")
|
||||||
* When this person was added
|
val createdAt: Long, // ← No default
|
||||||
*/
|
|
||||||
val createdAt: Long = System.currentTimeMillis(),
|
|
||||||
|
|
||||||
/**
|
@ColumnInfo(name = "updatedAt")
|
||||||
* Last time this person's data was updated
|
val updatedAt: Long // ← No default
|
||||||
*/
|
|
||||||
val updatedAt: Long = System.currentTimeMillis()
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Create PersonEntity with optional fields
|
|
||||||
*/
|
|
||||||
fun create(
|
fun create(
|
||||||
name: String,
|
name: String,
|
||||||
dateOfBirth: Long? = null,
|
dateOfBirth: Long? = null,
|
||||||
relationship: String? = null
|
relationship: String? = null
|
||||||
): PersonEntity {
|
): PersonEntity {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
return PersonEntity(
|
return PersonEntity(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
name = name,
|
name = name,
|
||||||
dateOfBirth = dateOfBirth,
|
dateOfBirth = dateOfBirth,
|
||||||
relationship = relationship
|
relationship = relationship,
|
||||||
|
createdAt = now,
|
||||||
|
updatedAt = now
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate age if date of birth is available
|
|
||||||
*/
|
|
||||||
fun getAge(): Int? {
|
fun getAge(): Int? {
|
||||||
if (dateOfBirth == null) return null
|
if (dateOfBirth == null) return null
|
||||||
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val ageInMillis = now - dateOfBirth
|
val ageInMillis = now - dateOfBirth
|
||||||
val ageInYears = ageInMillis / (1000L * 60 * 60 * 24 * 365)
|
return (ageInMillis / (1000L * 60 * 60 * 24 * 365)).toInt()
|
||||||
|
|
||||||
return ageInYears.toInt()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get relationship emoji
|
|
||||||
*/
|
|
||||||
fun getRelationshipEmoji(): String {
|
fun getRelationshipEmoji(): String {
|
||||||
return when (relationship) {
|
return when (relationship) {
|
||||||
"Family" -> "👨👩👧👦"
|
"Family" -> "👨👩👧👦"
|
||||||
@@ -104,11 +72,9 @@ data class PersonEntity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FaceModelEntity - Stores face recognition model (embedding) for a person
|
* FaceModelEntity - NO DEFAULT VALUES
|
||||||
*
|
|
||||||
* TABLE: face_models
|
|
||||||
* FOREIGN KEY: personId → persons.id
|
|
||||||
*/
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "face_models",
|
tableName = "face_models",
|
||||||
@@ -120,22 +86,36 @@ data class PersonEntity(
|
|||||||
onDelete = ForeignKey.CASCADE
|
onDelete = ForeignKey.CASCADE
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
indices = [
|
indices = [Index(value = ["personId"], unique = true)]
|
||||||
Index(value = ["personId"], unique = true)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
data class FaceModelEntity(
|
data class FaceModelEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: String = UUID.randomUUID().toString(),
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String, // ← No default
|
||||||
|
|
||||||
|
@ColumnInfo(name = "personId")
|
||||||
val personId: String,
|
val personId: String,
|
||||||
val embedding: String, // Serialized FloatArray
|
|
||||||
|
@ColumnInfo(name = "embedding")
|
||||||
|
val embedding: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "trainingImageCount")
|
||||||
val trainingImageCount: Int,
|
val trainingImageCount: Int,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "averageConfidence")
|
||||||
val averageConfidence: Float,
|
val averageConfidence: Float,
|
||||||
val createdAt: Long = System.currentTimeMillis(),
|
|
||||||
val updatedAt: Long = System.currentTimeMillis(),
|
@ColumnInfo(name = "createdAt")
|
||||||
val lastUsed: Long? = null,
|
val createdAt: Long, // ← No default
|
||||||
val isActive: Boolean = true
|
|
||||||
|
@ColumnInfo(name = "updatedAt")
|
||||||
|
val updatedAt: Long, // ← No default
|
||||||
|
|
||||||
|
@ColumnInfo(name = "lastUsed")
|
||||||
|
val lastUsed: Long?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "isActive")
|
||||||
|
val isActive: Boolean
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun create(
|
fun create(
|
||||||
@@ -144,11 +124,17 @@ data class FaceModelEntity(
|
|||||||
trainingImageCount: Int,
|
trainingImageCount: Int,
|
||||||
averageConfidence: Float
|
averageConfidence: Float
|
||||||
): FaceModelEntity {
|
): FaceModelEntity {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
return FaceModelEntity(
|
return FaceModelEntity(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
personId = personId,
|
personId = personId,
|
||||||
embedding = embeddingArray.joinToString(","),
|
embedding = embeddingArray.joinToString(","),
|
||||||
trainingImageCount = trainingImageCount,
|
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
|
* PhotoFaceTagEntity - NO DEFAULT VALUES
|
||||||
*
|
|
||||||
* TABLE: photo_face_tags
|
|
||||||
* FOREIGN KEYS:
|
|
||||||
* - imageId → images.imageId (String)
|
|
||||||
* - faceModelId → face_models.id (String)
|
|
||||||
*/
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "photo_face_tags",
|
tableName = "photo_face_tags",
|
||||||
@@ -190,18 +171,32 @@ data class FaceModelEntity(
|
|||||||
)
|
)
|
||||||
data class PhotoFaceTagEntity(
|
data class PhotoFaceTagEntity(
|
||||||
@PrimaryKey
|
@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 faceModelId: String,
|
||||||
|
|
||||||
val boundingBox: String, // "left,top,right,bottom"
|
@ColumnInfo(name = "boundingBox")
|
||||||
val confidence: Float,
|
val boundingBox: String,
|
||||||
val embedding: String, // Serialized FloatArray
|
|
||||||
|
|
||||||
val detectedAt: Long = System.currentTimeMillis(),
|
@ColumnInfo(name = "confidence")
|
||||||
val verifiedByUser: Boolean = false,
|
val confidence: Float,
|
||||||
val verifiedAt: Long? = null
|
|
||||||
|
@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 {
|
companion object {
|
||||||
fun create(
|
fun create(
|
||||||
@@ -212,11 +207,15 @@ data class PhotoFaceTagEntity(
|
|||||||
faceEmbedding: FloatArray
|
faceEmbedding: FloatArray
|
||||||
): PhotoFaceTagEntity {
|
): PhotoFaceTagEntity {
|
||||||
return PhotoFaceTagEntity(
|
return PhotoFaceTagEntity(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
imageId = imageId,
|
imageId = imageId,
|
||||||
faceModelId = faceModelId,
|
faceModelId = faceModelId,
|
||||||
boundingBox = "${boundingBox.left},${boundingBox.top},${boundingBox.right},${boundingBox.bottom}",
|
boundingBox = "${boundingBox.left},${boundingBox.top},${boundingBox.right},${boundingBox.bottom}",
|
||||||
confidence = confidence,
|
confidence = confidence,
|
||||||
embedding = faceEmbedding.joinToString(",")
|
embedding = faceEmbedding.joinToString(","),
|
||||||
|
detectedAt = System.currentTimeMillis(),
|
||||||
|
verifiedByUser = false,
|
||||||
|
verifiedAt = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -5,68 +5,113 @@ import androidx.room.Entity
|
|||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import java.util.UUID
|
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
|
* TagEntity - Normalized tag storage
|
||||||
*
|
*
|
||||||
* DESIGN:
|
* EXPLICIT COLUMN MAPPINGS for KSP compatibility
|
||||||
* - Tags exist once (e.g., "vacation")
|
|
||||||
* - Multiple images reference via ImageTagEntity junction table
|
|
||||||
* - Type system: GENERIC | SYSTEM | HIDDEN
|
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "tags")
|
@Entity(tableName = "tags")
|
||||||
data class TagEntity(
|
data class TagEntity(
|
||||||
|
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val tagId: String = UUID.randomUUID().toString(),
|
@ColumnInfo(name = "tagId")
|
||||||
|
val tagId: String,
|
||||||
|
|
||||||
/**
|
@ColumnInfo(name = "type")
|
||||||
* Tag type: GENERIC | SYSTEM | HIDDEN
|
val type: String,
|
||||||
*/
|
|
||||||
val type: String = TagType.GENERIC,
|
|
||||||
|
|
||||||
/**
|
@ColumnInfo(name = "value")
|
||||||
* Human-readable value, e.g. "vacation", "beach"
|
|
||||||
*/
|
|
||||||
val value: String,
|
val value: String,
|
||||||
|
|
||||||
/**
|
@ColumnInfo(name = "createdAt")
|
||||||
* When tag was created
|
val createdAt: Long
|
||||||
*/
|
|
||||||
val createdAt: Long = System.currentTimeMillis()
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a generic user tag
|
||||||
|
*/
|
||||||
fun createUserTag(value: String): TagEntity {
|
fun createUserTag(value: String): TagEntity {
|
||||||
return TagEntity(
|
return TagEntity(
|
||||||
|
tagId = UUID.randomUUID().toString(),
|
||||||
type = TagType.GENERIC,
|
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 {
|
fun createSystemTag(value: String): TagEntity {
|
||||||
return TagEntity(
|
return TagEntity(
|
||||||
|
tagId = UUID.randomUUID().toString(),
|
||||||
type = TagType.SYSTEM,
|
type = TagType.SYSTEM,
|
||||||
value = value.trim().lowercase()
|
value = value.trim().lowercase(),
|
||||||
|
createdAt = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create hidden tag (internal use)
|
||||||
|
*/
|
||||||
fun createHiddenTag(value: String): TagEntity {
|
fun createHiddenTag(value: String): TagEntity {
|
||||||
return TagEntity(
|
return TagEntity(
|
||||||
|
tagId = UUID.randomUUID().toString(),
|
||||||
type = TagType.HIDDEN,
|
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
|
fun isUserTag(): Boolean = type == TagType.GENERIC
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a system tag
|
||||||
|
*/
|
||||||
fun isSystemTag(): Boolean = type == TagType.SYSTEM
|
fun isSystemTag(): Boolean = type == TagType.SYSTEM
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a hidden tag
|
||||||
|
*/
|
||||||
fun isHiddenTag(): Boolean = type == TagType.HIDDEN
|
fun isHiddenTag(): Boolean = type == TagType.HIDDEN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display value (capitalized for UI)
|
||||||
|
*/
|
||||||
fun getDisplayValue(): String = value.replaceFirstChar { it.uppercase() }
|
fun getDisplayValue(): String = value.replaceFirstChar { it.uppercase() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TagWithUsage - For queries that include usage count
|
* 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(
|
data class TagWithUsage(
|
||||||
@ColumnInfo(name = "tagId")
|
@ColumnInfo(name = "tagId")
|
||||||
@@ -96,24 +141,3 @@ data class TagWithUsage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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"
|
|
||||||
}
|
|
||||||
@@ -52,7 +52,8 @@ class FaceRecognitionRepository @Inject constructor(
|
|||||||
): String = withContext(Dispatchers.IO) {
|
): String = withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
// Create PersonEntity with UUID
|
// Create PersonEntity with UUID
|
||||||
val person = PersonEntity(name = personName)
|
val person = PersonEntity.create(name = personName)
|
||||||
|
|
||||||
personDao.insert(person)
|
personDao.insert(person)
|
||||||
|
|
||||||
// Train face model
|
// Train face model
|
||||||
@@ -312,7 +313,10 @@ class FaceRecognitionRepository @Inject constructor(
|
|||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
suspend fun verifyFaceTag(tagId: String) {
|
suspend fun verifyFaceTag(tagId: String) {
|
||||||
photoFaceTagDao.markTagAsVerified(tagId)
|
photoFaceTagDao.markTagAsVerified(
|
||||||
|
tagId = tagId,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUnverifiedTags(): List<PhotoFaceTagEntity> {
|
suspend fun getUnverifiedTags(): List<PhotoFaceTagEntity> {
|
||||||
|
|||||||
@@ -12,97 +12,68 @@ import dagger.hilt.components.SingletonComponent
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DatabaseModule - Provides database and DAOs
|
* DatabaseModule - Provides database and ALL DAOs
|
||||||
*
|
*
|
||||||
* FRESH START VERSION:
|
* DEVELOPMENT CONFIGURATION:
|
||||||
* - No migration needed
|
* - fallbackToDestructiveMigration enabled
|
||||||
* - Uses fallbackToDestructiveMigration (deletes old database)
|
* - No migrations required
|
||||||
* - Perfect for development
|
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object DatabaseModule {
|
object DatabaseModule {
|
||||||
|
|
||||||
|
// ===== DATABASE =====
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatabase(
|
fun provideDatabase(
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext context: Context
|
||||||
): AppDatabase {
|
): AppDatabase =
|
||||||
return Room.databaseBuilder(
|
Room.databaseBuilder(
|
||||||
context,
|
context,
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"sherpai.db"
|
"sherpai.db"
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration() // ← Deletes old database, creates fresh
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
}
|
|
||||||
|
|
||||||
// ===== YOUR EXISTING DAOs =====
|
// ===== CORE DAOs =====
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideImageDao(database: AppDatabase): ImageDao {
|
fun provideImageDao(db: AppDatabase): ImageDao =
|
||||||
return database.imageDao()
|
db.imageDao()
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideTagDao(database: AppDatabase): TagDao {
|
fun provideTagDao(db: AppDatabase): TagDao =
|
||||||
return database.tagDao()
|
db.tagDao()
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideEventDao(database: AppDatabase): EventDao {
|
fun provideEventDao(db: AppDatabase): EventDao =
|
||||||
return database.eventDao()
|
db.eventDao()
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideImageTagDao(database: AppDatabase): ImageTagDao {
|
fun provideImageEventDao(db: AppDatabase): ImageEventDao =
|
||||||
return database.imageTagDao()
|
db.imageEventDao()
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideImagePersonDao(database: AppDatabase): ImagePersonDao {
|
fun provideImageAggregateDao(db: AppDatabase): ImageAggregateDao =
|
||||||
return database.imagePersonDao()
|
db.imageAggregateDao()
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideImageEventDao(database: AppDatabase): ImageEventDao {
|
fun provideImageTagDao(db: AppDatabase): ImageTagDao =
|
||||||
return database.imageEventDao()
|
db.imageTagDao()
|
||||||
}
|
|
||||||
|
// ===== FACE RECOGNITION DAOs =====
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideImageAggregateDao(database: AppDatabase): ImageAggregateDao {
|
fun providePersonDao(db: AppDatabase): PersonDao =
|
||||||
return database.imageAggregateDao()
|
db.personDao()
|
||||||
}
|
|
||||||
|
|
||||||
// ===== NEW FACE RECOGNITION DAOs =====
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providePersonDao(database: AppDatabase): PersonDao {
|
fun provideFaceModelDao(db: AppDatabase): FaceModelDao =
|
||||||
return database.personDao()
|
db.faceModelDao()
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideFaceModelDao(database: AppDatabase): FaceModelDao {
|
fun providePhotoFaceTagDao(db: AppDatabase): PhotoFaceTagDao =
|
||||||
return database.faceModelDao()
|
db.photoFaceTagDao()
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
fun providePhotoFaceTagDao(database: AppDatabase): PhotoFaceTagDao {
|
|
||||||
return database.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
|
|
||||||
*/
|
|
||||||
@@ -1,32 +1,37 @@
|
|||||||
package com.placeholder.sherpai2.ui.presentation
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
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.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.placeholder.sherpai2.ui.navigation.AppNavHost
|
import com.placeholder.sherpai2.ui.navigation.AppNavHost
|
||||||
import com.placeholder.sherpai2.ui.navigation.AppRoutes
|
import com.placeholder.sherpai2.ui.navigation.AppRoutes
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beautiful main screen with gradient header, dynamic actions, and polish
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen() {
|
fun MainScreen() {
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// Navigation controller for NavHost
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
// Track current backstack entry to update top bar title dynamically
|
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentRoute = navBackStackEntry?.destination?.route ?: AppRoutes.SEARCH
|
val currentRoute = navBackStackEntry?.destination?.route ?: AppRoutes.SEARCH
|
||||||
|
|
||||||
// Drawer content for navigation
|
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
@@ -37,7 +42,6 @@ fun MainScreen() {
|
|||||||
drawerState.close()
|
drawerState.close()
|
||||||
if (route != currentRoute) {
|
if (route != currentRoute) {
|
||||||
navController.navigate(route) {
|
navController.navigate(route) {
|
||||||
// Avoid multiple copies of the same destination
|
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,17 +50,120 @@ fun MainScreen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
// Main scaffold with top bar
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(currentRoute.replaceFirstChar { it.uppercase() }) },
|
title = {
|
||||||
navigationIcon = {
|
Column {
|
||||||
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
Text(
|
||||||
Icon(Icons.Filled.Menu, contentDescription = "Open Drawer")
|
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.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 ->
|
) { paddingValues ->
|
||||||
AppNavHost(
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ fun FacePickerScreen(
|
|||||||
// from the Bitmap scale to the UI View scale.
|
// from the Bitmap scale to the UI View scale.
|
||||||
Canvas(modifier = Modifier.fillMaxSize().clickable { /* Handle general tap */ }) {
|
Canvas(modifier = Modifier.fillMaxSize().clickable { /* Handle general tap */ }) {
|
||||||
// Implementation of coordinate mapping goes here
|
// 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
|
// Simplified: Just show the options as a list of crops if Canvas mapping is too complex for now
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ plugins {
|
|||||||
//https://github.com/google/dagger/issues/4048#issuecomment-1864237679
|
//https://github.com/google/dagger/issues/4048#issuecomment-1864237679
|
||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
alias(libs.plugins.hilt.android) apply false
|
alias(libs.plugins.hilt.android) apply false
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -21,3 +21,4 @@ kotlin.code.style=official
|
|||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
org.gradle.java.home=/snap/android-studio/current/jbr
|
||||||
|
|||||||
Reference in New Issue
Block a user