Oh yes - Thats how we do

No default params for KSP complainer fuck

UI sweeps
This commit is contained in:
genki
2026-01-09 19:59:44 -05:00
parent 51fdfbf3d6
commit 52ea64f29a
14 changed files with 389 additions and 284 deletions

View File

@@ -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

View File

@@ -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>>
}
}

View File

@@ -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)
@@ -50,4 +48,4 @@ interface PersonDao {
@Query("SELECT EXISTS(SELECT 1 FROM persons WHERE id = :personId)")
suspend fun personExists(personId: String): Boolean
}
}

View File

@@ -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,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<PhotoFaceTagEntity>
suspend fun getRecentlyDetectedFaces(limit: Int): List<PhotoFaceTagEntity>
}
/**
* Simple data class for photo counts
*/
data class FaceModelPhotoCount(
val faceModelId: String,
val photoCount: Int
)
)

View File

@@ -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>
}

View File

@@ -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
)
}
}

View File

@@ -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
)

View File

@@ -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"
}

View File

@@ -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> {

View File

@@ -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
*/

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
android.nonTransitiveRClass=true
org.gradle.java.home=/snap/android-studio/current/jbr