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
|
||||
PhotoFaceTagEntity::class // NEW: Face tags
|
||||
],
|
||||
version = 4,
|
||||
version = 5,
|
||||
exportSchema = false
|
||||
)
|
||||
// No TypeConverters needed - embeddings stored as strings
|
||||
|
||||
@@ -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<List<ImageTagEntity>>
|
||||
|
||||
/**
|
||||
* 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<String>
|
||||
|
||||
@Transaction
|
||||
@@ -49,7 +44,4 @@ interface ImageTagDao {
|
||||
WHERE it.imageId = :imageId AND it.visibility = 'PUBLIC'
|
||||
""")
|
||||
fun getTagsForImage(imageId: String): Flow<List<TagEntity>>
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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<PersonEntity>)
|
||||
@@ -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)
|
||||
|
||||
@@ -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<PhotoFaceTagEntity>)
|
||||
@@ -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<PhotoFaceTagEntity>
|
||||
suspend fun getLowConfidenceTags(threshold: Float): List<PhotoFaceTagEntity>
|
||||
|
||||
@Query("SELECT * FROM photo_face_tags WHERE verifiedByUser = 0 ORDER BY detectedAt DESC")
|
||||
suspend fun getUnverifiedTags(): List<PhotoFaceTagEntity>
|
||||
@@ -78,13 +78,13 @@ 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<PhotoFaceTagEntity>
|
||||
suspend fun getRecentlyDetectedFaces(limit: Int): List<PhotoFaceTagEntity>
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple data class for photo counts
|
||||
*/
|
||||
data class FaceModelPhotoCount(
|
||||
val faceModelId: String,
|
||||
val photoCount: Int
|
||||
|
||||
@@ -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<TagWithUsage>
|
||||
suspend fun getMostUsedTags(limit: Int): List<TagWithUsage>
|
||||
|
||||
/**
|
||||
* 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<TagEntity>
|
||||
|
||||
/**
|
||||
* 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<TagEntity>
|
||||
|
||||
// ======================
|
||||
@@ -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<TagEntity>
|
||||
suspend fun searchTags(query: String, limit: Int): List<TagEntity>
|
||||
|
||||
/**
|
||||
* 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<TagWithUsage>
|
||||
suspend fun searchTagsWithUsage(query: String, limit: Int): List<TagWithUsage>
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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")
|
||||
@@ -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) {
|
||||
|
||||
// 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<PhotoFaceTagEntity> {
|
||||
|
||||
@@ -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()
|
||||
fun providePhotoFaceTagDao(db: AppDatabase): PhotoFaceTagDao =
|
||||
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
|
||||
|
||||
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() }) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
||||
Icon(Icons.Filled.Menu, contentDescription = "Open Drawer")
|
||||
}
|
||||
}
|
||||
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.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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -21,3 +21,4 @@ kotlin.code.style=official
|
||||
# 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
|
||||
org.gradle.java.home=/snap/android-studio/current/jbr
|
||||
|
||||
Reference in New Issue
Block a user