4 Commits

Author SHA1 Message Date
genki
49e6523f75 Adding HILT DI and First Pass at backend Kotlin 2025-12-24 10:18:46 -05:00
genki
c10cbf373f Working Gallery and Repo - Earlydays! 2025-12-20 18:27:09 -05:00
genki
91f6327c31 CheckPoint save for adding 'Tour' screen, and PhotoData and PhotoViewModels 2025-12-20 18:27:09 -05:00
genki
52fa755a3f Working Gallery and Repo - Earlydays! 2025-12-20 17:57:01 -05:00
47 changed files with 1546 additions and 608 deletions

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
/.idea/

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
SherpAI2

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@@ -1,77 +1,75 @@
// build.gradle.kts (Module: :app)
plugins { plugins {
// 1. Core Android and Kotlin plugins (MUST be first) alias(libs.plugins.android.application)
id("com.android.application") alias(libs.plugins.kotlin.android)
kotlin("android") alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
id("org.jetbrains.kotlin.plugin.compose") // Note: No version is specified here alias(libs.plugins.hilt.android)
} }
android { android {
// 2. Android Configuration
namespace = "com.placeholder.sherpai2" namespace = "com.placeholder.sherpai2"
compileSdk = 34 compileSdk = 36 // SDK 35 is the stable standard for 2025; 36 is preview
defaultConfig { defaultConfig {
applicationId = "com.placeholder.sherpai2" applicationId = "com.placeholder.sherpai2"
minSdk = 24 minSdk = 24
targetSdk = 34 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
// 3. Kotlin & Java Settings
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions {
jvmTarget = "1.8" kotlinOptions {
jvmTarget = "17"
} }
// 4. Jetpack Compose Configuration (Crucial!)
buildFeatures { buildFeatures {
compose = true compose = true
} }
composeOptions { androidResources {
kotlinCompilerExtensionVersion = "1.5.8" // Must match your Kotlin version noCompress += "tflite"
} }
} }
dependencies { // FIX for hiltAggregateDepsDebug: Correctly configure the Hilt extension
// --- CORE ANDROID & LIFECYCLE --- hilt {
implementation("androidx.core:core-ktx:1.12.0") enableAggregatingTask = false
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") }
implementation("androidx.activity:activity-compose:1.8.2") // Fixes 'activity' ref error
dependencies {
// --- JETPACK COMPOSE UI (Material 3) --- // Compose
implementation("androidx.compose.ui:ui") implementation(platform(libs.androidx.compose.bom))
implementation("androidx.compose.ui:ui-graphics") implementation(libs.androidx.core.ktx)
implementation("androidx.compose.ui:ui-tooling-preview") implementation(libs.androidx.lifecycle.runtime)
implementation("androidx.compose.material3:material3") // Fixes 'material3' ref error implementation(libs.androidx.activity.compose)
implementation(libs.compose.ui)
// --- COMPOSE ICONS (Fixes 'material' and 'Icons' ref errors) --- implementation(libs.compose.material3)
// Uses direct string to avoid Version Catalog conflicts implementation(libs.compose.icons)
implementation("androidx.compose.material:material-icons-extended:1.6.0") implementation(libs.compose.navigation)
debugImplementation(libs.compose.ui.tooling)
// --- STATE MANAGEMENT / COROUTINES ---
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") // Camera & ML
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation(libs.camera.core)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation(libs.camera.lifecycle)
implementation(libs.camera.view)
// --- TESTING --- implementation(libs.mlkit.face)
testImplementation("junit:junit:4.13.2") implementation(libs.tflite)
androidTestImplementation("androidx.test.ext:junit:1.1.5") implementation(libs.tflite.support)
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.compose.ui:ui-test-junit4") // Room (KSP)
debugImplementation("androidx.compose.ui:ui-tooling") implementation(libs.room.runtime)
debugImplementation("androidx.compose.ui:ui-test-manifest") ksp(libs.room.compiler)
implementation("androidx.compose.foundation:foundation:1.6.0") // Use your current Compose version // Images
implementation("androidx.compose.material3:material3:1.2.1") // <-- Fix/Reconfirm Material 3 implementation(libs.coil.compose)
implementation("io.coil-kt:coil-compose:2.6.0") // Hilt (KSP) - Fixed by removing kapt and using ksp
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
} }

View File

@@ -10,7 +10,8 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SherpAI2"> android:theme="@style/Theme.SherpAI2"
android:name=".SherpAIApplication">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -23,5 +24,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
</manifest> </manifest>

View File

@@ -1,30 +1,72 @@
package com.placeholder.sherpai2 package com.placeholder.sherpai2
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import com.placeholder.sherpai2.presentation.MainScreen // IMPORT your main screen import com.placeholder.sherpai2.presentation.MainScreen // IMPORT your main screen
import com.placeholder.sherpai2.ui.theme.SherpAI2Theme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// 1. Define the permission needed based on API level
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
setContent { setContent {
// Assume you have a Theme file named SherpAI2Theme (standard for new projects) SherpAI2Theme {
// Replace with your actual project theme if different // 2. State to track if permission is granted
MaterialTheme { var hasPermission by remember {
Surface( mutableStateOf(ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED)
modifier = Modifier.fillMaxSize(), }
color = MaterialTheme.colorScheme.background
) { // 3. Launcher to ask for permission
// Launch the main navigation UI val launcher = rememberLauncherForActivityResult(
MainScreen() ActivityResultContracts.RequestPermission()
) { isGranted ->
hasPermission = isGranted
}
// 4. Trigger request on start
LaunchedEffect(Unit) {
if (!hasPermission) launcher.launch(permission)
}
if (hasPermission) {
MainScreen() // Your existing screen that holds MainContentArea
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Please grant storage permission to view photos.")
} }
} }
} }
}
} }
} }

View File

@@ -0,0 +1,7 @@
package com.placeholder.sherpai2
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class SherpAIApplication : Application()

View File

@@ -0,0 +1,58 @@
package com.placeholder.sherpai2.data.di
import android.content.Context
import com.placeholder.sherpai2.data.local.FaceDao
import com.placeholder.sherpai2.data.local.FaceDatabase
import com.placeholder.sherpai2.data.repo.FaceRepository
import com.placeholder.sherpai2.domain.faces.analyzer.FaceAnalyzer
import com.placeholder.sherpai2.domain.faces.ml.FaceNetInterpreter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
// ---------- Database ----------
@Provides
@Singleton
fun provideFaceDatabase(
@ApplicationContext context: Context
): FaceDatabase =
FaceDatabase.getInstance(context)
@Provides
fun provideFaceDao(
db: FaceDatabase
): FaceDao = db.faceDao()
// ---------- Repository ----------
@Provides
@Singleton
fun provideFaceRepository(
dao: FaceDao
): FaceRepository =
FaceRepository(dao)
// ---------- ML ----------
@Provides
@Singleton
fun provideFaceNetInterpreter(
@ApplicationContext context: Context
): FaceNetInterpreter =
FaceNetInterpreter(context)
@Provides
@Singleton
fun provideFaceAnalyzer(
@ApplicationContext context: Context
): FaceAnalyzer =
FaceAnalyzer(context)
}

View File

@@ -0,0 +1,31 @@
package com.placeholder.sherpai2.data.local
import androidx.room.TypeConverter
import java.nio.ByteBuffer
/**
* Converts FloatArray to ByteArray and back for Room persistence.
*/
object Converters {
@TypeConverter
@JvmStatic
fun fromFloatArray(value: FloatArray): ByteArray {
val buffer = ByteBuffer.allocate(value.size * 4)
for (f in value) {
buffer.putFloat(f)
}
return buffer.array()
}
@TypeConverter
@JvmStatic
fun toFloatArray(bytes: ByteArray): FloatArray {
val buffer = ByteBuffer.wrap(bytes)
val floats = FloatArray(bytes.size / 4)
for (i in floats.indices) {
floats[i] = buffer.getFloat()
}
return floats
}
}

View File

@@ -0,0 +1,26 @@
package com.placeholder.sherpai2.data.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
/**
* DAO for face embeddings.
*/
@Dao
interface FaceDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(face: FaceEntity): Long
@Query("SELECT * FROM faces")
suspend fun getAllFaces(): List<FaceEntity>
@Query("SELECT * FROM faces WHERE label = :label LIMIT 1")
suspend fun getFaceByLabel(label: String): FaceEntity?
@Query("DELETE FROM faces")
suspend fun clearAll()
}

View File

@@ -0,0 +1,38 @@
package com.placeholder.sherpai2.data.local
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
/**
* Room database for storing face embeddings.
*/
@Database(
entities = [FaceEntity::class],
version = 1,
exportSchema = false
)
abstract class FaceDatabase : RoomDatabase() {
abstract fun faceDao(): FaceDao
companion object {
@Volatile
private var INSTANCE: FaceDatabase? = null
fun getInstance(context: Context): FaceDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
FaceDatabase::class.java,
"face_database"
)
.fallbackToDestructiveMigration() // Safe for dev; can be removed in prod
.build()
INSTANCE = instance
instance
}
}
}
}

View File

@@ -0,0 +1,23 @@
package com.placeholder.sherpai2.data.local
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
/**
* Room entity representing a single face embedding.
*
* @param id Auto-generated primary key
* @param label Name or identifier for the face
* @param embedding FloatArray of length 128/512 representing the face
*/
@Entity(tableName = "faces")
@TypeConverters(Converters::class)
data class FaceEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val label: String,
val embedding: FloatArray
)

View File

@@ -1,10 +1,13 @@
package com.placeholder.sherpai2.data.photos package com.placeholder.sherpai2.data.photos
import android.net.Uri
data class Photo( data class Photo(
val id: String, val id: Long,
val uri: String, val uri: Uri,
val title: String? = null, val title: String? = null,
val timestamp: Long val size: Long,
val dateModified: Int
) )
data class Album( data class Album(

View File

@@ -1,25 +0,0 @@
package com.placeholder.sherpai2.data.photos
import com.placeholder.sherpai2.data.photos.Photo
object SamplePhotoSource {
fun loadPhotos(): List<Photo> {
return listOf(
Photo(
id = "1",
uri = "file:///sdcard/Pictures/20200115_181335.jpg",
title = "Sample One",
timestamp = System.currentTimeMillis()
),
Photo(
id = "2",
uri = "file:///sdcard/Pictures/20200115_181417.jpg",
title = "Sample Two",
timestamp = System.currentTimeMillis()
)
)
}
}
// /sdcard/Pictures/20200115_181335.jpg
// /sdcard/Pictures/20200115_181417.jpg

View File

@@ -0,0 +1,90 @@
package com.placeholder.sherpai2.data.repo
import com.placeholder.sherpai2.data.local.FaceDao
import com.placeholder.sherpai2.data.local.FaceEntity
import com.placeholder.sherpai2.domain.util.EmbeddingMath
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Repository for managing face embeddings.
*
* Handles:
* - Saving new embeddings
* - Querying embeddings
* - Matching a new embedding against stored embeddings
*/
class FaceRepository(
private val faceDao: FaceDao
) {
/**
* Save a new face embedding with the given label.
*/
suspend fun saveFace(label: String, embedding: FloatArray): Long =
withContext(Dispatchers.IO) {
EmbeddingMath.l2Normalize(embedding)
val entity = FaceEntity(label = label, embedding = embedding)
faceDao.insert(entity)
}
/**
* Retrieve all stored embeddings.
*/
suspend fun getAllFaces(): List<FaceEntity> =
withContext(Dispatchers.IO) {
faceDao.getAllFaces()
}
/**
* Find the most similar stored face to the given embedding.
*
* Returns the matched FaceEntity and similarity metrics,
* or null if no match exceeds the provided thresholds.
*/
suspend fun findClosestMatch(
embedding: FloatArray,
cosineThreshold: Float = 0.80f,
euclideanThreshold: Float = 1.10f
): FaceMatchResult? = withContext(Dispatchers.IO) {
EmbeddingMath.l2Normalize(embedding)
val faces = faceDao.getAllFaces()
if (faces.isEmpty()) return@withContext null
var bestMatch: FaceEntity? = null
var bestCosine = -1f
var bestEuclidean = Float.MAX_VALUE
for (face in faces) {
val storedEmbedding = face.embedding.copyOf()
EmbeddingMath.l2Normalize(storedEmbedding)
val cosine = EmbeddingMath.cosineSimilarity(embedding, storedEmbedding)
val euclidean = EmbeddingMath.euclideanDistance(embedding, storedEmbedding)
if (cosine > bestCosine && euclidean < euclideanThreshold && cosine >= cosineThreshold) {
bestCosine = cosine
bestEuclidean = euclidean
bestMatch = face
}
}
bestMatch?.let {
FaceMatchResult(
face = it,
cosineSimilarity = bestCosine,
euclideanDistance = bestEuclidean
)
}
}
}
/**
* Result of a face comparison.
*/
data class FaceMatchResult(
val face: FaceEntity,
val cosineSimilarity: Float,
val euclideanDistance: Float
)

View File

@@ -1,11 +1,58 @@
package com.placeholder.sherpai2.data.repo package com.placeholder.sherpai2.data.repo
import android.content.Context
import android.provider.MediaStore
import android.content.ContentUris
import android.net.Uri
import android.os.Environment
import android.util.Log
import com.placeholder.sherpai2.data.photos.Photo import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.data.photos.SamplePhotoSource import com.placeholder.sherpai2.domain.PhotoDuplicateScanner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
class PhotoRepository { class PhotoRepository(private val context: Context) {
fun getPhotos(): List<Photo> { fun scanExternalStorage(): Result<List<Photo>> {
return SamplePhotoSource.loadPhotos() // Best Practice: Use Environment.getExternalStorageDirectory()
// only as a fallback or starting point for legacy support.
val rootPath = Environment.getExternalStorageDirectory()
return runCatching {
val photos = mutableListOf<Photo>()
if (rootPath.exists() && rootPath.isDirectory) {
// walkTopDown is efficient but can throw AccessDeniedException
rootPath.walkTopDown()
.maxDepth(3) // Performance Best Practice: Don't scan the whole phone
.filter { it.isFile && isImageFile(it.extension) }
.forEach { file ->
photos.add(mapFileToPhoto(file))
} }
}
photos
}.onFailure { e ->
Log.e("PhotoRepo", "Failed to scan filesystem", e)
}
}
private fun mapFileToPhoto(file: File): Photo {
return Photo(
id = file.path.hashCode().toLong(),
uri = Uri.fromFile(file),
title = file.name,
size = file.length(),
dateModified = (file.lastModified() / 1000).toInt()
)
}
fun findDuplicates(photos: Photo) {
TODO("Not yet implemented")
}
fun fetchPhotos() {
TODO("Not yet implemented")
}
private fun isImageFile(ext: String) = listOf("jpg", "jpeg", "png").contains(ext.lowercase())
} }

View File

@@ -0,0 +1,3 @@
package com.placeholder.sherpai2.domain
//fun getAllPhotos(context: Context): List<Photo> {

View File

@@ -0,0 +1,19 @@
package com.placeholder.sherpai2.domain
import android.content.Context
import com.placeholder.sherpai2.data.photos.Photo
class PhotoDuplicateScanner(private val context: Context) {
/**
* Finds duplicate photos by grouping them by file size or name.
* In a production app, you might use MD5 hashes or PHash for visual similarity.
*/
fun findDuplicates(allPhotos: List<Photo>): Map<String, List<Photo>> {
// Grouping by size is a fast, common way to find potential duplicates
return allPhotos
.groupBy { it.size } // Groups photos that have the exact same byte size
.filter { it.value.size > 1 } // Only keep groups where more than one photo was found
.mapKeys { "Size: ${it.key} bytes" } // Create a readable label for the group
}
}

View File

@@ -0,0 +1,2 @@
package com.placeholder.sherpai2.domain.faces

View File

@@ -0,0 +1,113 @@
package com.placeholder.sherpai2.domain.faces.analyzer
import android.content.Context
import android.graphics.Bitmap
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.Face
import com.google.mlkit.vision.face.FaceDetection
import com.google.mlkit.vision.face.FaceDetector
import com.google.mlkit.vision.face.FaceDetectorOptions
import com.placeholder.sherpai2.domain.faces.ml.FaceNetInterpreter
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* Orchestrates the full face analysis pipeline for still images:
*
* 1. Detect faces using ML Kit
* 2. Select dominant face
* 3. Crop face from bitmap
* 4. Generate FaceNet embedding
*
* This class contains no UI logic and no persistence logic.
*/
class FaceAnalyzer(
context: Context
) {
private val faceDetector: FaceDetector
private val faceNet: FaceNetInterpreter
init {
val options = FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
.build()
faceDetector = FaceDetection.getClient(options)
faceNet = FaceNetInterpreter(context)
}
/**
* Entry point: analyze a bitmap and return its face embedding.
*
* @throws IllegalStateException if no face is detected
*/
suspend fun analyze(bitmap: Bitmap): FloatArray {
val faces = detectFaces(bitmap)
if (faces.isEmpty()) {
throw IllegalStateException("No face detected in image")
}
val primaryFace = selectDominantFace(faces)
val croppedFace = cropFace(bitmap, primaryFace)
val inputBuffer = faceNet.bitmapToInputBuffer(croppedFace)
return faceNet.runEmbedding(inputBuffer)
}
/**
* Runs ML Kit face detection on a still image.
*/
private suspend fun detectFaces(bitmap: Bitmap): List<Face> =
suspendCancellableCoroutine { cont ->
val image = InputImage.fromBitmap(bitmap, 0)
faceDetector.process(image)
.addOnSuccessListener { faces ->
cont.resume(faces)
}
.addOnFailureListener { e ->
cont.resumeWithException(e)
}
}
/**
* Selects the largest face by bounding box area.
*/
private fun selectDominantFace(faces: List<Face>): Face {
return faces.maxBy { face ->
face.boundingBox.width() * face.boundingBox.height()
}
}
/**
* Crops the detected face region from the original bitmap.
* Bounds are clamped to avoid IllegalArgumentException.
*/
private fun cropFace(bitmap: Bitmap, face: Face): Bitmap {
val box = face.boundingBox
val left = box.left.coerceAtLeast(0)
val top = box.top.coerceAtLeast(0)
val right = box.right.coerceAtMost(bitmap.width)
val bottom = box.bottom.coerceAtMost(bitmap.height)
val width = (right - left).coerceAtLeast(1)
val height = (bottom - top).coerceAtLeast(1)
return Bitmap.createBitmap(bitmap, left, top, width, height)
}
/**
* Explicit cleanup hook.
*/
fun close() {
faceDetector.close()
faceNet.close()
}
}

View File

@@ -0,0 +1,111 @@
package com.placeholder.sherpai2.domain.faces.ml
import android.content.Context
import android.graphics.Bitmap
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.support.common.FileUtil
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* Thin wrapper around TensorFlow Lite Interpreter.
* Responsible ONLY for:
* - Loading the model
* - Preparing input buffers
* - Running inference
*/
class FaceNetInterpreter(
context: Context
) {
private val interpreter: Interpreter
init {
val modelBuffer = FileUtil.loadMappedFile(
context,
ModelConstants.MODEL_FILE_NAME
)
val options = Interpreter.Options().apply {
setNumThreads(4)
// GPU delegate intentionally NOT enabled yet
}
interpreter = Interpreter(modelBuffer, options)
}
/**
* Converts a face bitmap into a normalized input buffer.
* Expects a tightly-cropped face.
*/
fun bitmapToInputBuffer(bitmap: Bitmap): ByteBuffer {
val resized = Bitmap.createScaledBitmap(
bitmap,
ModelConstants.INPUT_WIDTH,
ModelConstants.INPUT_HEIGHT,
true
)
val buffer = ByteBuffer.allocateDirect(
1 *
ModelConstants.INPUT_WIDTH *
ModelConstants.INPUT_HEIGHT *
ModelConstants.INPUT_CHANNELS *
4
).apply {
order(ByteOrder.nativeOrder())
}
val pixels = IntArray(
ModelConstants.INPUT_WIDTH * ModelConstants.INPUT_HEIGHT
)
resized.getPixels(
pixels,
0,
ModelConstants.INPUT_WIDTH,
0,
0,
ModelConstants.INPUT_WIDTH,
ModelConstants.INPUT_HEIGHT
)
for (pixel in pixels) {
buffer.putFloat(
((pixel shr 16 and 0xFF) - ModelConstants.IMAGE_MEAN) /
ModelConstants.IMAGE_STD
)
buffer.putFloat(
((pixel shr 8 and 0xFF) - ModelConstants.IMAGE_MEAN) /
ModelConstants.IMAGE_STD
)
buffer.putFloat(
((pixel and 0xFF) - ModelConstants.IMAGE_MEAN) /
ModelConstants.IMAGE_STD
)
}
buffer.rewind()
return buffer
}
/**
* Runs FaceNet inference and returns the embedding vector.
*/
fun runEmbedding(inputBuffer: ByteBuffer): FloatArray {
val output = Array(1) {
FloatArray(ModelConstants.EMBEDDING_SIZE)
}
interpreter.run(inputBuffer, output)
return output[0]
}
/**
* Clean up explicitly when the app shuts down.
*/
fun close() {
interpreter.close()
}
}

View File

@@ -0,0 +1,27 @@
package com.placeholder.sherpai2.domain.faces.ml
/**
* Centralized constants for FaceNet-style models.
* Changing models should only require edits in this file.
*/
object ModelConstants {
// FaceNet standard input size
const val INPUT_WIDTH = 160
const val INPUT_HEIGHT = 160
const val INPUT_CHANNELS = 3
// Output embedding size (FaceNet variants are typically 128 or 512)
const val EMBEDDING_SIZE = 128
// Normalization constants (FaceNet expects [-1, 1])
const val IMAGE_MEAN = 127.5f
const val IMAGE_STD = 128.0f
// Asset path
const val MODEL_FILE_NAME = "facenet_model.tflite"
// Similarity thresholds (tunable later)
const val COSINE_SIMILARITY_THRESHOLD = 0.80f
const val EUCLIDEAN_DISTANCE_THRESHOLD = 1.10f
}

View File

@@ -0,0 +1,59 @@
package com.placeholder.sherpai2.domain.util
import kotlin.math.sqrt
/**
* Utilities for operating on FaceNet-style embeddings.
*
* All functions are pure and testable.
*/
object EmbeddingMath {
/**
* L2-normalizes a float array in place.
* Ensures embedding vectors have unit length for cosine similarity.
*/
fun l2Normalize(embedding: FloatArray) {
var sum = 0.0
for (v in embedding) {
sum += (v * v)
}
val norm = sqrt(sum)
if (norm > 0.0) {
for (i in embedding.indices) {
embedding[i] = (embedding[i] / norm).toFloat()
}
}
}
/**
* Computes cosine similarity between two embeddings.
* Both embeddings should be L2-normalized for correct results.
* Returns value in [-1, 1], higher = more similar.
*/
fun cosineSimilarity(a: FloatArray, b: FloatArray): Float {
require(a.size == b.size) { "Embedding size mismatch: ${a.size} != ${b.size}" }
var dot = 0.0f
for (i in a.indices) {
dot += a[i] * b[i]
}
return dot
}
/**
* Computes Euclidean distance between two embeddings.
* Returns a non-negative float; smaller = more similar.
*/
fun euclideanDistance(a: FloatArray, b: FloatArray): Float {
require(a.size == b.size) { "Embedding size mismatch: ${a.size} != ${b.size}" }
var sum = 0.0f
for (i in a.indices) {
val diff = a[i] - b[i]
sum += diff * diff
}
return sqrt(sum)
}
}

View File

@@ -1,42 +0,0 @@
// In navigation/AppDestinations.kt
package com.placeholder.sherpai2.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.ui.graphics.vector.ImageVector
/**
* Defines all navigation destinations (screens) for the application.
*/
sealed class AppDestinations(val route: String, val icon: ImageVector, val label: String) {
// Core Functional Sections
object Tour : AppDestinations(
route = "Tour",
icon = Icons.Default.PhotoLibrary,
label = "Tour"
)
object Search : AppDestinations("search", Icons.Default.Search, "Search")
object Models : AppDestinations("models", Icons.Default.Layers, "Models")
object Inventory : AppDestinations("inv", Icons.Default.Inventory2, "Inv")
object Train : AppDestinations("train", Icons.Default.TrackChanges, "Train")
object Tags : AppDestinations("tags", Icons.Default.LocalOffer, "Tags")
// Utility/Secondary Sections
object Upload : AppDestinations("upload", Icons.Default.CloudUpload, "Upload")
object Settings : AppDestinations("settings", Icons.Default.Settings, "Settings")
}
// Lists used by the AppDrawerContent to render the menu sections easily
val mainDrawerItems = listOf(
AppDestinations.Tour,
AppDestinations.Search,
AppDestinations.Models,
AppDestinations.Inventory,
AppDestinations.Train,
AppDestinations.Tags
)
val utilityDrawerItems = listOf(
AppDestinations.Upload,
AppDestinations.Settings
)

View File

@@ -1,57 +0,0 @@
// In presentation/MainScreen.kt
package com.placeholder.sherpai2.presentation
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.placeholder.sherpai2.navigation.AppDestinations
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
// State to track which screen is currently visible
// var currentScreen by remember { mutableStateOf(AppDestinations.Search) }
var currentScreen: AppDestinations by remember { mutableStateOf(AppDestinations.Search) }
// ModalNavigationDrawer provides the left sidebar UI/UX
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
// The content of the drawer (AppDrawerContent)
AppDrawerContent(
currentScreen = currentScreen,
onDestinationClicked = { destination ->
currentScreen = destination
scope.launch { drawerState.close() } // Close drawer after selection
}
)
},
) {
// The main content area
Scaffold(
topBar = {
TopAppBar(
title = { Text(currentScreen.label) },
// Button to open the drawer
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Filled.Menu, contentDescription = "Open Drawer")
}
}
)
}
) { paddingValues ->
// Displays the content for the currently selected screen
MainContentArea(
currentScreen = currentScreen,
modifier = Modifier.padding(paddingValues)
)
}
}
}

View File

@@ -1,104 +0,0 @@
// In presentation/MainContentArea.kt
package com.placeholder.sherpai2.presentation
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.placeholder.sherpai2.navigation.AppDestinations
import com.placeholder.sherpai2.ui.theme.SherpAI2Theme
@Composable
fun MainContentArea(currentScreen: AppDestinations, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
// Swaps the UI content based on the selected screen from the drawer
when (currentScreen) {
AppDestinations.Tour -> TwinAlbumScreen("New Collections." , "Classics")
AppDestinations.Search -> SimplePlaceholder("Search for your photo.")
AppDestinations.Models -> SimplePlaceholder("Models Screen: Manage your LoRA/embeddings.")
AppDestinations.Inventory -> SimplePlaceholder("Inventory Screen: View all collected data.")
AppDestinations.Train -> SimplePlaceholder("Train Screen: Start the LoRA adaptation process.")
AppDestinations.Tags -> SimplePlaceholder("Tags Screen: Create and edit custom tags.")
AppDestinations.Upload -> SimplePlaceholder("Upload Screen: Import new photos/data.")
AppDestinations.Settings -> SimplePlaceholder("Settings Screen: Configure app behavior.")
}
}
}
@Composable
private fun SimplePlaceholder(text: String) {
Text(
text = text,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp)
)
}
@Composable
fun AlbumPreview(title: String, color: Color) {
Box(
modifier = Modifier
.padding(8.dp)
.aspectRatio(1f) // Makes it a square
.background(color, shape = RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center
) {
Text(text = title, color = Color.White, fontWeight = FontWeight.Bold)
}
}
@Composable
fun TwinAlbumScreen(albumNameA: String, albumNameB: String) {
Column(modifier = Modifier.fillMaxSize()) {
// --- Top 40% Section ---
Row(
modifier = Modifier
.fillMaxWidth()
.weight(0.30f) // This takes exactly 40% of vertical space
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically
) {
// These two occupy the 40% area
Box(modifier = Modifier.weight(1f)) {
AlbumPreview("Mackenzie Hazer", Color.Blue)
}
Box(modifier = Modifier.weight(1f)) {
AlbumPreview("Winifred", Color.Magenta)
}
}
// --- Bottom 60% Section ---
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.6f) // This takes the remaining 60%
.background(Color.LightGray.copy(alpha = 0.2f))
) {
Text("Other content goes here", modifier = Modifier.align(Alignment.Center))
}
}
}
@Preview
@Composable
fun PreviewTwinTopRow() {
SherpAI2Theme {
TwinAlbumScreen("Album A", "Album B")
}
}

View File

@@ -1,35 +0,0 @@
package com.placeholder.sherpai2.presentation
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.data.photos.SamplePhotoSource
@Composable
fun PhotoListScreen(
photos: List<Photo>
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp)
) {
items(photos, key = { it.id }) { photo ->
Image(
painter = rememberAsyncImagePainter(photo.uri),
contentDescription = photo.title,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(bottom = 8.dp)
)
}
}
}

View File

@@ -0,0 +1,33 @@
// In navigation/AppDestinations.kt
package com.placeholder.sherpai2.ui.navigation
/**
* Defines all navigation destinations (screens) for the application.
*/
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.ui.graphics.vector.ImageVector
/**
* Defines all navigation destinations (screens) for the application.
* Changed to 'enum class' to enable built-in iteration (.entries) for NavHost.
*/
enum class AppDestinations(val route: String, val icon: ImageVector, val label: String) {
// Core Functional Sections
Tour("tour", Icons.Default.PhotoLibrary, "Tour"),
Search("search", Icons.Default.Search, "Search"),
Models("models", Icons.Default.Layers, "Models"),
Inventory("inv", Icons.Default.Inventory2, "Inventory"),
Train("train", Icons.Default.TrackChanges, "Train"),
Tags("tags", Icons.Default.LocalOffer, "Tags"),
// Utility/Secondary Sections
Upload("upload", Icons.Default.CloudUpload, "Upload"),
Settings("settings", Icons.Default.Settings, "Settings");
companion object {
// High-level grouping for the Drawer UI
val mainDrawerItems = listOf(Tour, Search, Models, Inventory, Train, Tags)
val utilityDrawerItems = listOf(Upload, Settings)
}
}

View File

@@ -1,35 +1,32 @@
// In presentation/AppDrawerContent.kt // In presentation/AppDrawerContent.kt
package com.placeholder.sherpai2.presentation package com.placeholder.sherpai2.ui.presentation
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.placeholder.sherpai2.navigation.AppDestinations import com.placeholder.sherpai2.ui.navigation.AppDestinations
import com.placeholder.sherpai2.navigation.mainDrawerItems
import com.placeholder.sherpai2.navigation.utilityDrawerItems
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppDrawerContent( fun AppDrawerContent(
currentScreen: AppDestinations, currentScreen: AppDestinations,
onDestinationClicked: (AppDestinations) -> Unit onDestinationClicked: (AppDestinations) -> Unit
) { ) {
// Defines the width and content of the sliding drawer panel
ModalDrawerSheet(modifier = Modifier.width(280.dp)) { ModalDrawerSheet(modifier = Modifier.width(280.dp)) {
// Header/Logo Area // Header Area
Text( Text(
"SherpAI Control Panel", text = "SherpAI Control Panel",
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
Divider(Modifier.fillMaxWidth())
// 1. Main Navigation Items HorizontalDivider(modifier = Modifier.fillMaxWidth())
// 1. Main Navigation Items (Referencing the Companion Object)
Column(modifier = Modifier.padding(vertical = 8.dp)) { Column(modifier = Modifier.padding(vertical = 8.dp)) {
mainDrawerItems.forEach { destination -> AppDestinations.mainDrawerItems.forEach { destination ->
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(destination.label) }, label = { Text(destination.label) },
icon = { Icon(destination.icon, contentDescription = destination.label) }, icon = { Icon(destination.icon, contentDescription = destination.label) },
@@ -41,11 +38,15 @@ fun AppDrawerContent(
} }
// Separator // Separator
Divider(Modifier.fillMaxWidth().padding(vertical = 8.dp)) HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
// 2. Utility Items // 2. Utility Items (Referencing the Companion Object)
Column(modifier = Modifier.padding(vertical = 8.dp)) { Column(modifier = Modifier.padding(vertical = 8.dp)) {
utilityDrawerItems.forEach { destination -> AppDestinations.utilityDrawerItems.forEach { destination ->
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(destination.label) }, label = { Text(destination.label) },
icon = { Icon(destination.icon, contentDescription = destination.label) }, icon = { Icon(destination.icon, contentDescription = destination.label) },

View File

@@ -0,0 +1,78 @@
// In presentation/MainContentArea.kt
package com.placeholder.sherpai2.ui.presentation
import GalleryScreen
import GalleryViewModel
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.placeholder.sherpai2.ui.navigation.AppDestinations
import com.placeholder.sherpai2.ui.screens.managephotos.ManagePhotosScreen
import com.placeholder.sherpai2.ui.screens.managephotos.ManagePhotosViewModel
@Composable
fun MainContentArea(
navController: NavHostController, // Standard MAD practice: pass the controller
modifier: Modifier = Modifier
) {
// NavHost acts as the "Gallery Building"
NavHost(
navController = navController,
startDestination = AppDestinations.Tour.route, // Using your enum routes
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
// Destination: Tour (Gallery)
composable(AppDestinations.Tour.route) {
val galleryViewModel: GalleryViewModel = viewModel()
val uiState by galleryViewModel.uiState.collectAsState()
GalleryScreen(state = uiState)
}
// Destination: Inventory (Manage Photos)
composable(AppDestinations.Inventory.route) {
// New ViewModel scoped to this specific screen package
val manageViewModel: ManagePhotosViewModel = viewModel()
val manageState by manageViewModel.uiState.collectAsState()
ManagePhotosScreen(
state = manageState,
onCleanUpClick = { manageViewModel.performCleanUp() },
onCountTagsClick = { manageViewModel.countByTag() },
onStatsClick = { manageViewModel.loadStats() }
)
}
// Placeholders for other routes
composable(AppDestinations.Search.route) { SimplePlaceholder("Find Any Photos.") }
composable(AppDestinations.Settings.route) { SimplePlaceholder("Settings Screen.") }
// ... add other destinations similarly
}
}
@Composable
private fun SimplePlaceholder(text: String) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = text,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.padding(16.dp)
.background(color = Color.Magenta.copy(alpha = 0.2f))
)
}
}

View File

@@ -0,0 +1,74 @@
// In presentation/MainScreen.kt
package com.placeholder.sherpai2.presentation
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.placeholder.sherpai2.ui.navigation.AppDestinations
import com.placeholder.sherpai2.ui.presentation.AppDrawerContent
import com.placeholder.sherpai2.ui.presentation.MainContentArea
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
// 1. The 'Tour Guide' (NavController) replaces the 'currentScreen' state
val navController = rememberNavController()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
// 2. Observe the backstack to determine the current screen for the UI (TopBar/Drawer)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route ?: AppDestinations.Search.route
// Find the current destination object based on the route string
val currentScreen = AppDestinations.values().find { it.route == currentRoute } ?: AppDestinations.Search
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
AppDrawerContent(
currentScreen = currentScreen,
onDestinationClicked = { destination ->
// 3. Best Practice: Navigate with state restoration
navController.navigate(destination.route) {
// Pop up to the start destination to avoid building a huge backstack
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when re-selecting
launchSingleTop = true
// Restore state when re-selecting a previously selected item
restoreState = true
}
scope.launch { drawerState.close() }
}
)
},
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(currentScreen.label) },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Filled.Menu, contentDescription = "Open Drawer")
}
}
)
}
) { paddingValues ->
// 4. Pass the navController to the Content Area
MainContentArea(
navController = navController,
modifier = Modifier.padding(paddingValues)
)
}
}
}

View File

@@ -0,0 +1,124 @@
package com.placeholder.sherpai2.ui.screens.managephotos
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.placeholder.sherpai2.data.repo.PhotoRepository
import com.placeholder.sherpai2.ui.screens.tourscreen.GalleryUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Analytics
import androidx.compose.material.icons.filled.CleaningServices
import androidx.compose.material.icons.filled.Label
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
// Change this line to include '.navigation'
@Composable
fun ManagePhotosScreen(
state: ManagePhotosUiState,
onCleanUpClick: () -> Unit,
onCountTagsClick: () -> Unit,
onStatsClick: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Manage Photos",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 24.dp)
)
// --- Top Action Buttons Row ---
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
ManageActionButton(
text = "Clean Up",
icon = Icons.Default.CleaningServices,
modifier = Modifier.weight(1f),
onClick = onCleanUpClick
)
ManageActionButton(
text = "Count by Tag",
icon = Icons.Default.Label,
modifier = Modifier.weight(1f),
onClick = onCountTagsClick
)
ManageActionButton(
text = "Stats",
icon = Icons.Default.Analytics,
modifier = Modifier.weight(1f),
onClick = onStatsClick
)
}
Spacer(modifier = Modifier.height(32.dp))
// --- Dynamic Content Area (Based on State) ---
Box(modifier = Modifier.fillMaxSize()) {
when (state) {
is ManagePhotosUiState.Idle -> {
Text("Select an action above to begin.", Modifier.align(Alignment.Center))
}
is ManagePhotosUiState.Scanning -> {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
is ManagePhotosUiState.Success -> {
DuplicateList(state.duplicateGroups)
}
is ManagePhotosUiState.Error -> {
}
is ManagePhotosUiState.Loading -> {
}
}
}
}
}
@Composable
fun ManageActionButton(
text: String,
icon: ImageVector,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
OutlinedButton(
onClick = onClick,
modifier = modifier.height(80.dp),
shape = MaterialTheme.shapes.medium,
contentPadding = PaddingValues(8.dp)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(icon, contentDescription = null)
Spacer(Modifier.height(4.dp))
Text(text = text, style = MaterialTheme.typography.labelSmall)
}
}
}
@Composable
fun DuplicateList(duplicates: Map<String, List<*>>) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
item { Text("Found ${duplicates.size} Duplicate Groups", style = MaterialTheme.typography.titleMedium) }
// We will build out the actual GroupCard next
}
}

View File

@@ -0,0 +1,20 @@
package com.placeholder.sherpai2.ui.screens.managephotos
import com.placeholder.sherpai2.data.photos.Photo
sealed class ManagePhotosUiState {
object Idle : ManagePhotosUiState()
object Scanning : ManagePhotosUiState()
object Loading : ManagePhotosUiState()
data class Success(
val duplicateGroups: Map<String, List<Photo>> = emptyMap(),
val stats: PhotoStats? = null
) : ManagePhotosUiState()
data class Error(val message: String) : ManagePhotosUiState()
}
data class PhotoStats(
val totalPhotos: Int,
val totalSize: Long,
val tagCounts: Map<String, Int>
)

View File

@@ -0,0 +1,65 @@
package com.placeholder.sherpai2.ui.screens.managephotos
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.data.repo.PhotoRepository
import com.placeholder.sherpai2.domain.PhotoDuplicateScanner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class ManagePhotosViewModel(application: Application) : AndroidViewModel(application) {
private val repository = PhotoRepository(application)
private val scanner = PhotoDuplicateScanner(application)
private val _uiState = MutableStateFlow<ManagePhotosUiState>(ManagePhotosUiState.Idle)
val uiState: StateFlow<ManagePhotosUiState> = _uiState.asStateFlow()
fun performCleanUp() {
viewModelScope.launch {
_uiState.value = ManagePhotosUiState.Loading
// Use the existing method from your PhotoRepository
val result = repository.scanExternalStorage()
// Handle the Result type properly
val allPhotos = result.getOrNull() ?: emptyList()
// Pass the List<Photo> to the scanner
val duplicates = scanner.findDuplicates(allPhotos)
// Update state with the correct property name: duplicateGroups
_uiState.value = ManagePhotosUiState.Success(duplicateGroups = duplicates)
}
}
fun countByTag() {
// Logic for tagging implementation goes here
}
fun loadStats() {
viewModelScope.launch {
_uiState.value = ManagePhotosUiState.Loading
val result = repository.scanExternalStorage()
val allPhotos: List<Photo> = result.getOrNull() ?: emptyList()
// sumOf now works because allPhotos is a List<Photo>, not Unit
val totalSize = allPhotos.sumOf { it.size }
_uiState.value = ManagePhotosUiState.Success(
stats = PhotoStats(
totalPhotos = allPhotos.size,
totalSize = totalSize,
tagCounts = emptyMap() // Implement tagging logic later
)
)
}
}
}

View File

@@ -0,0 +1,79 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.ui.screens.tourscreen.GalleryUiState
@Composable
fun GalleryScreen(
state: GalleryUiState,
modifier: Modifier = Modifier
) {
// Column ensures the Header and the Grid are stacked vertically
Column(modifier = modifier.fillMaxSize()) {
Text(
text = "Photo Gallery",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(16.dp)
)
Box(
modifier = Modifier.weight(1f), // Fills remaining space, allowing the grid to scroll
contentAlignment = Alignment.Center
) {
when (state) {
is GalleryUiState.Loading -> CircularProgressIndicator()
is GalleryUiState.Error -> Text(text = state.message, color = MaterialTheme.colorScheme.error)
is GalleryUiState.Success -> {
if (state.photos.isEmpty()) {
Text("No photos found.")
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize(),
// contentPadding prevents the bottom items from being cut off by the navigation bar
contentPadding = PaddingValues(bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
// Using photo.uri as the key for better scroll performance
items(state.photos, key = { it.uri }) { photo ->
PhotoItem(photo)
}
}
}
}
}
}
}
}
@Composable
fun PhotoItem(photo: Photo) {
AsyncImage(
model = photo.uri,
contentDescription = photo.title,
modifier = Modifier
.aspectRatio(1f) // Makes it a square
.fillMaxWidth(),
contentScale = ContentScale.Crop
)
}

View File

@@ -0,0 +1,9 @@
package com.placeholder.sherpai2.ui.screens.tourscreen
import com.placeholder.sherpai2.data.photos.Photo
sealed class GalleryUiState {
object Loading : GalleryUiState()
data class Success(val photos: List<Photo>) : GalleryUiState()
data class Error(val message: String) : GalleryUiState()
}

View File

@@ -0,0 +1,33 @@
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.placeholder.sherpai2.data.repo.PhotoRepository
import com.placeholder.sherpai2.ui.screens.tourscreen.GalleryUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class GalleryViewModel(application: Application) : AndroidViewModel(application) {
// Initialize repository with the application context
private val repository = PhotoRepository(application)
private val _uiState = MutableStateFlow<GalleryUiState>(GalleryUiState.Loading)
val uiState = _uiState.asStateFlow()
init {
loadPhotos()
}
fun loadPhotos() {
viewModelScope.launch {
val result = repository.scanExternalStorage()
result.onSuccess { photos ->
_uiState.value = GalleryUiState.Success(photos)
}.onFailure { error ->
_uiState.value = GalleryUiState.Error(error.message ?: "Unknown Error")
}
}
}
}

View File

@@ -0,0 +1,4 @@
package com.placeholder.sherpai2.ui.screens.tourscreen.components
class AlbumBox {
}

View File

@@ -0,0 +1,44 @@
package com.placeholder.sherpai2.ui.screens.trainscreen
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.placeholder.sherpai2.data.repo.FaceRepository
import com.placeholder.sherpai2.domain.faces.analyzer.FaceAnalyzer
import kotlinx.coroutines.launch
@Composable
fun ImagePickerScreen(
viewModel: ImagePickerViewModel
) {
val selectedImages by viewModel.selectedImages
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents()
) { uris: List<Uri> ->
viewModel.onImagesSelected(uris)
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Button(onClick = { launcher.launch("image/*") }) {
Text("Select Photos")
}
Spacer(modifier = Modifier.height(16.dp))
LazyColumn {
items(selectedImages) { uri ->
Text(uri.toString(), style = MaterialTheme.typography.bodyMedium)
}
}
}
}

View File

@@ -0,0 +1,67 @@
package com.placeholder.sherpai2.ui.screens.trainscreen
import android.net.Uri
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.placeholder.sherpai2.data.repo.FaceRepository
import com.placeholder.sherpai2.domain.faces.analyzer.FaceAnalyzer
import kotlinx.coroutines.launch
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.os.Build
import android.provider.MediaStore
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class ImagePickerViewModel @Inject constructor(
@ApplicationContext private val appContext: Context,
private val faceAnalyzer: FaceAnalyzer,
private val repository: FaceRepository
) : ViewModel() {
private val _selectedImages = mutableStateOf<List<Uri>>(emptyList())
val selectedImages: State<List<Uri>> = _selectedImages
fun onImagesSelected(uris: List<Uri>) {
_selectedImages.value = uris.take(10)
viewModelScope.launch {
uris.take(10).forEach { uri ->
val bitmap = loadBitmapFromUri(uri)
val embedding = faceAnalyzer.analyze(bitmap)
val label = uri.lastPathSegment ?: "Unknown"
repository.saveFace(label, embedding)
}
}
}
private suspend fun loadBitmapFromUri(uri: Uri): Bitmap =
withContext(Dispatchers.IO) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val source = ImageDecoder.createSource(
appContext.contentResolver,
uri
)
ImageDecoder.decodeBitmap(source)
} else {
@Suppress("DEPRECATION")
MediaStore.Images.Media.getBitmap(
appContext.contentResolver,
uri
)
}
}
}

View File

@@ -0,0 +1,2 @@
package com.placeholder.sherpai2.ui.screens.trainscreen

View File

@@ -1,141 +0,0 @@
package com.placeholder.sherpai2.ui.tourscreen
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.placeholder.sherpai2.data.photos.Album
import com.placeholder.sherpai2.ui.theme.SherpAI2Theme
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.placeholder.sherpai2.presentation.AlbumPreview
class GalleryScreen {
}
@Composable
fun GalleryContent(topAlbums: List<Album>) {
Column(modifier = Modifier.fillMaxSize()) {
// --- Top 40% Section ---
Row(
modifier = Modifier
.fillMaxWidth()
.weight(0.4f)
.padding(8.dp)
) {
// We use .getOrNull to safely check if the albums exist in the list
val firstAlbum = topAlbums.getOrNull(0)
val secondAlbum = topAlbums.getOrNull(1)
if (firstAlbum != null) {
AlbumPreviewBoxRandom(album = firstAlbum, modifier = Modifier.weight(1f))
} else {
Box(modifier = Modifier.weight(1f)) // Empty placeholder
}
if (secondAlbum != null) {
AlbumPreviewBoxRandom(album = secondAlbum, modifier = Modifier.weight(1f))
} else {
Box(modifier = Modifier.weight(1f)) // Empty placeholder
}
}
// --- Bottom 60% Section ---
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.6f)
) {
Text("Lower Content Area", modifier = Modifier.align(Alignment.Center))
}
}
}
// --- STATEFUL (Use this for Production) ---
@Composable
fun GalleryScreen(viewModel: GalleryViewModel = viewModel()) {
val albumList by viewModel.albums
GalleryContent(topAlbums = albumList)
}
// --- PREVIEW ---
@Preview(showBackground = true)
@Composable
fun GalleryPreview() {
SherpAI2Theme {
// Feed mock data into the Stateless version
GalleryContent(topAlbums = listOf(/* Fake Album Objects */))
}
}
@Composable
fun AlbumPreviewBox(
album: Album,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.padding(8.dp)
.aspectRatio(1f), // Keeps it square
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.DarkGray), // Background until image is loaded
contentAlignment = Alignment.BottomStart
) {
// Title Overlay
Text(
text = album.title,
modifier = Modifier
.fillMaxWidth()
.background(Color.Black.copy(alpha = 0.5f))
.padding(8.dp),
color = Color.White,
style = MaterialTheme.typography.labelLarge
)
}
}
}
@Composable
fun AlbumPreviewBoxRandom(album: Album, modifier: Modifier = Modifier) {
Card(
modifier = modifier.padding(8.dp).aspectRatio(1f),
shape = RoundedCornerShape(12.dp)
) {
Row(modifier = Modifier.fillMaxSize()) {
// Loop through the 3 random photos in the album
album.photos.forEach { photo ->
AsyncImage(
model = photo.uri,
contentDescription = null,
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
contentScale = ContentScale.Crop // Fills the space nicely
)
}
}
}
}

View File

@@ -1,97 +0,0 @@
package com.placeholder.sherpai2.ui.tourscreen
import android.os.Environment
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.placeholder.sherpai2.data.photos.AlbumPhoto
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import com.placeholder.sherpai2.data.photos.Album // Import your data model
import com.placeholder.sherpai2.data.photos.Photo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class GalleryViewModel : ViewModel() {
// This is the 'albums' reference your UI is looking for
private val _albums = mutableStateOf<List<Album>>(emptyList())
val albums: State<List<Album>> = _albums
init {
fetchInitialData()
}
private fun fetchInitialData() {
viewModelScope.launch(Dispatchers.IO) {
val directory = File("/sdcard/photos") // Or: Environment.getExternalStorageDirectory()
if (directory.exists() && directory.isDirectory) {
val photoFiles = directory.listFiles { file ->
file.extension.lowercase() in listOf("jpg", "jpeg", "png")
} ?: emptyArray()
// Map files to your Photo data class
val loadedPhotos = photoFiles.map { file ->
Photo(
id = file.absolutePath,
uri = file.toURI().toString(),
title = file.nameWithoutExtension,
timestamp = file.lastModified()
)
}
// Create an Album object with these photos
val mainAlbum = Album(
id = "local_photos",
title = "Phone Gallery",
photos = loadedPhotos
)
// Update the UI State on the Main Thread
withContext(Dispatchers.Main) {
_albums.value = listOf(mainAlbum)
}
}
}
}
private fun fetchInitialDataRandom() {
viewModelScope.launch(Dispatchers.IO) {
val root = Environment.getExternalStorageDirectory()
val directory = File(root, "photos")
if (directory.exists() && directory.isDirectory) {
// 1. Filter specifically for .jpg files
val photoFiles = directory.listFiles { file ->
file.extension.lowercase() == "jpg"
} ?: emptyArray()
// 2. Shuffle the list and take only the first 3
val randomPhotos = photoFiles.asSequence()
.shuffled()
.take(3)
.map { file ->
Photo(
id = file.absolutePath,
uri = file.toURI().toString(),
title = file.nameWithoutExtension,
timestamp = file.lastModified()
)
}.toList()
// 3. Update the state with these 3 random photos
val mainAlbum = Album(
id = "random_selection",
title = "Random Picks",
photos = randomPhotos
)
withContext(Dispatchers.Main) {
_albums.value = listOf(mainAlbum)
}
}
}
}
}

View File

@@ -1,4 +0,0 @@
package com.placeholder.sherpai2.ui.tourscreen.components
class AlbumBox {
}

View File

@@ -4,3 +4,9 @@ plugins {
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
} }
buildscript {
dependencies {
classpath("com.android.tools.build:gradle:8.13.2")
}
}

View File

@@ -0,0 +1,65 @@
[versions]
activityComposeVersion = "1.8.2"
agp = "8.13.1"
coreKtxVersion = "1.12.0"
kotlin = "2.0.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.1"
composeBom = "2024.09.00"
materialIconsExtended = "1.6.0"
navigationRuntimeKtx = "2.9.6"
navigationCompose = "2.9.6"
#Branch Updated -dev_backend_one
ksp = "2.0.21-1.0.28" # MUST match the 2.0.21 prefix
room = "2.6.1"
camerax = "1.4.0"
[libraries]
androidx-activity-compose-v182 = { module = "androidx.activity:activity-compose", version.ref = "activityComposeVersion" }
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-ktx-v1120 = { module = "androidx.core:core-ktx", version.ref = "coreKtxVersion" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
#Branch Updated -dev_backend_one
# CameraX stack
camera-core = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
# ML Stack
mlkit-face = { group = "com.google.mlkit", name = "face-detection", version = "16.1.7" }
tflite = { group = "org.tensorflow", name = "tensorflow-lite", version = "2.16.1" }
# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

View File

@@ -1,32 +1,66 @@
[versions] [versions]
agp = "8.13.1" # Tooling
agp = "8.7.3" # Latest stable for 2025
kotlin = "2.0.21" kotlin = "2.0.21"
coreKtx = "1.17.0" ksp = "2.0.21-1.0.28" # Strictly matched to Kotlin version
junit = "4.13.2"
junitVersion = "1.3.0" # AndroidX / Compose
espressoCore = "3.7.0" androidx-core = "1.15.0"
lifecycleRuntimeKtx = "2.10.0" androidx-lifecycle = "2.10.0"
activityCompose = "1.12.1" androidx-activity = "1.12.2"
composeBom = "2024.09.00" compose-bom = "2025.12.00"
navigation = "2.8.5"
# Camera & ML
camerax = "1.5.2"
mlkit-face = "16.1.7"
tflite = "2.16.1"
# Database & DI
room = "2.7.0-alpha01"
hilt = "2.54" # Supports KSP2 and fixes JavaPoet conflicts
# Images
coil = "2.6.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } # Core & Lifecycle
junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } # Compose
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } compose-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } # CameraX
camera-core = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
# ML
mlkit-face = { group = "com.google.mlkit", name = "face-detection", version.ref = "mlkit-face" }
tflite = { group = "org.tensorflow", name = "tensorflow-lite", version.ref = "tflite" }
tflite-support = { group = "org.tensorflow", name = "tensorflow-lite-support", version = "0.4.4" }
# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
# Images
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }