1 Commits

Author SHA1 Message Date
genki
16eecf6c08 CheckPoint save for adding 'Tour' screen, and PhotoData and PhotoViewModels 2025-12-20 11:39:46 -05:00
41 changed files with 422 additions and 1024 deletions

1
.idea/.name generated
View File

@@ -1 +0,0 @@
SherpAI2

View File

@@ -1,4 +0,0 @@
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,76 +1,77 @@
// build.gradle.kts (Module: :app)
plugins { plugins {
alias(libs.plugins.android.application) // 1. Core Android and Kotlin plugins (MUST be first)
alias(libs.plugins.kotlin.android) id("com.android.application")
alias(libs.plugins.kotlin.compose) kotlin("android")
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android) id("org.jetbrains.kotlin.plugin.compose") // Note: No version is specified here
} }
android { android {
// 2. Android Configuration
namespace = "com.placeholder.sherpai2" namespace = "com.placeholder.sherpai2"
compileSdk = 35 compileSdk = 34
defaultConfig { defaultConfig {
applicationId = "com.placeholder.sherpai2" applicationId = "com.placeholder.sherpai2"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { // 3. Kotlin & Java Settings
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "1.8"
} }
// 4. Jetpack Compose Configuration (Crucial!)
buildFeatures { buildFeatures {
compose = true compose = true
} }
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8" // Must match your Kotlin version
}
} }
dependencies { dependencies {
// AndroidX & Lifecycle // --- CORE ANDROID & LIFECYCLE ---
implementation(libs.androidx.core.ktx) implementation("androidx.core:core-ktx:1.12.0")
implementation(libs.androidx.lifecycle.runtime.ktx) implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation("androidx.activity:activity-compose:1.8.2") // Fixes 'activity' ref error
implementation(libs.androidx.activity.compose)
// Compose (Using BOM) // --- JETPACK COMPOSE UI (Material 3) ---
implementation(platform(libs.androidx.compose.bom)) implementation("androidx.compose.ui:ui")
implementation(libs.androidx.compose.ui) implementation("androidx.compose.ui:ui-graphics")
implementation(libs.androidx.compose.ui.graphics) implementation("androidx.compose.ui:ui-tooling-preview")
implementation(libs.androidx.compose.ui.tooling.preview) implementation("androidx.compose.material3:material3") // Fixes 'material3' ref error
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons)
// Navigation & Hilt Integration // --- COMPOSE ICONS (Fixes 'material' and 'Icons' ref errors) ---
implementation(libs.androidx.navigation.compose) // Uses direct string to avoid Version Catalog conflicts
implementation(libs.androidx.hilt.navigation.compose) implementation("androidx.compose.material:material-icons-extended:1.6.0")
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
// Images // --- STATE MANAGEMENT / COROUTINES ---
implementation(libs.coil.compose) implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Tooling (Fixed to use TOML alias) // --- TESTING ---
debugImplementation(libs.androidx.compose.ui.tooling) testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
//backend2 implementation("androidx.compose.foundation:foundation:1.6.0") // Use your current Compose version
//room addon implementation("androidx.compose.material3:material3:1.2.1") // <-- Fix/Reconfirm Material 3
implementation(libs.room.runtime)
implementation(libs.room.ktx) implementation("io.coil-kt:coil-compose:2.6.0")
ksp(libs.room.compiler)
} }

View File

@@ -23,6 +23,5 @@
</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,70 +1,30 @@
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
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 {
SherpAI2Theme { // Assume you have a Theme file named SherpAI2Theme (standard for new projects)
// 2. State to track if permission is granted // Replace with your actual project theme if different
var hasPermission by remember { MaterialTheme {
mutableStateOf(ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Launch the main navigation UI
MainScreen()
} }
// 3. Launcher to ask for permission
val launcher = rememberLauncherForActivityResult(
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

@@ -1,47 +0,0 @@
package com.placeholder.sherpai2.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
import com.placeholder.sherpai2.data.local.dao.EventDao
import com.placeholder.sherpai2.data.local.dao.ImageAggregateDao
import com.placeholder.sherpai2.data.local.dao.ImageDao
import com.placeholder.sherpai2.data.local.dao.ImageEventDao
import com.placeholder.sherpai2.data.local.dao.ImagePersonDao
import com.placeholder.sherpai2.data.local.dao.ImageTagDao
import com.placeholder.sherpai2.data.local.dao.PersonDao
import com.placeholder.sherpai2.data.local.dao.TagDao
import com.placeholder.sherpai2.data.local.entity.EventEntity
import com.placeholder.sherpai2.data.local.entity.ImageEntity
import com.placeholder.sherpai2.data.local.entity.ImageEventEntity
import com.placeholder.sherpai2.data.local.entity.ImagePersonEntity
import com.placeholder.sherpai2.data.local.entity.ImageTagEntity
import com.placeholder.sherpai2.data.local.entity.PersonEntity
import com.placeholder.sherpai2.data.local.entity.TagEntity
@Database(
entities = [
ImageEntity::class,
TagEntity::class,
PersonEntity::class,
EventEntity::class,
ImageTagEntity::class,
ImagePersonEntity::class,
ImageEventEntity::class
],
version = 1,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun imageDao(): ImageDao
abstract fun tagDao(): TagDao
abstract fun personDao(): PersonDao
abstract fun eventDao(): EventDao
abstract fun imageTagDao(): ImageTagDao
abstract fun imagePersonDao(): ImagePersonDao
abstract fun imageEventDao(): ImageEventDao
abstract fun imageAggregateDao(): ImageAggregateDao
}

View File

@@ -1,26 +0,0 @@
package com.placeholder.sherpai2.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.placeholder.sherpai2.data.local.entity.EventEntity
@Dao
interface EventDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(event: EventEntity)
/**
* Find events covering a timestamp.
*
* This is the backbone of auto-tagging by date.
*/
@Query("""
SELECT * FROM events
WHERE :timestamp BETWEEN startDate AND endDate
AND isHidden = 0
""")
suspend fun findEventsForTimestamp(timestamp: Long): List<EventEntity>
}

View File

@@ -1,25 +0,0 @@
package com.placeholder.sherpai2.data.local.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import com.placeholder.sherpai2.data.local.model.ImageWithEverything
import kotlinx.coroutines.flow.Flow
@Dao
interface ImageAggregateDao {
/**
* Observe a fully-hydrated image object.
*
* This is what your detail screen should use.
*/
@Transaction
@Query("""
SELECT * FROM images
WHERE imageId = :imageId
""")
fun observeImageWithEverything(
imageId: String
): Flow<ImageWithEverything>
}

View File

@@ -1,56 +0,0 @@
package com.placeholder.sherpai2.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.placeholder.sherpai2.data.local.entity.ImageEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface ImageDao {
/**
* Insert images.
*
* IGNORE prevents duplicate insertion
* when sha256 or imageUri already exists.
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertImages(images: List<ImageEntity>)
/**
* Get image by ID.
*/
@Query("SELECT * FROM images WHERE imageId = :imageId")
suspend fun getImageById(imageId: String): ImageEntity?
/**
* Stream images ordered by capture time (newest first).
*
* Flow is critical:
* - UI auto-updates
* - No manual refresh
*/
@Query("""
SELECT * FROM images
ORDER BY capturedAt DESC
""")
fun observeAllImages(): Flow<List<ImageEntity>>
/**
* Fetch images in a time range.
* Used for:
* - event auto-assignment
* - timeline views
*/
@Query("""
SELECT * FROM images
WHERE capturedAt BETWEEN :start AND :end
ORDER BY capturedAt ASC
""")
suspend fun getImagesInRange(
start: Long,
end: Long
): List<ImageEntity>
}

View File

@@ -1,23 +0,0 @@
package com.placeholder.sherpai2.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.placeholder.sherpai2.data.local.entity.ImageEventEntity
@Dao
interface ImageEventDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(entity: ImageEventEntity)
/**
* Images associated with an event.
*/
@Query("""
SELECT imageId FROM image_events
WHERE eventId = :eventId
""")
suspend fun findImagesForEvent(eventId: String): List<String>
}

View File

@@ -1,25 +0,0 @@
package com.placeholder.sherpai2.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.placeholder.sherpai2.data.local.entity.ImagePersonEntity
@Dao
interface ImagePersonDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(entity: ImagePersonEntity)
/**
* All images containing a specific person.
*/
@Query("""
SELECT imageId FROM image_persons
WHERE personId = :personId
AND visibility = 'PUBLIC'
AND confirmed = 1
""")
suspend fun findImagesForPerson(personId: String): List<String>
}

View File

@@ -1,41 +0,0 @@
package com.placeholder.sherpai2.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.placeholder.sherpai2.data.local.entity.ImageTagEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface ImageTagDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(imageTag: ImageTagEntity)
/**
* Observe tags for an image.
*/
@Query("""
SELECT * FROM image_tags
WHERE imageId = :imageId
AND visibility != 'HIDDEN'
""")
fun observeTagsForImage(imageId: String): Flow<List<ImageTagEntity>>
/**
* Find images by tag.
*
* This is your primary tag-search query.
*/
@Query("""
SELECT imageId FROM image_tags
WHERE tagId = :tagId
AND visibility = 'PUBLIC'
AND confidence >= :minConfidence
""")
suspend fun findImagesByTag(
tagId: String,
minConfidence: Float = 0.5f
): List<String>
}

View File

@@ -1,24 +0,0 @@
package com.placeholder.sherpai2.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.placeholder.sherpai2.data.local.entity.PersonEntity
@Dao
interface PersonDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(person: PersonEntity)
@Query("SELECT * FROM persons WHERE personId = :personId")
suspend fun getById(personId: String): PersonEntity?
@Query("""
SELECT * FROM persons
WHERE isHidden = 0
ORDER BY displayName
""")
suspend fun getVisiblePeople(): List<PersonEntity>
}

View File

@@ -1,24 +0,0 @@
package com.placeholder.sherpai2.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.placeholder.sherpai2.data.local.entity.TagEntity
@Dao
interface TagDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(tag: TagEntity)
/**
* Resolve a tag by value.
* Example: "park"
*/
@Query("SELECT * FROM tags WHERE value = :value LIMIT 1")
suspend fun getByValue(value: String): TagEntity?
@Query("SELECT * FROM tags")
suspend fun getAll(): List<TagEntity>
}

View File

@@ -1,44 +0,0 @@
package com.placeholder.sherpai2.data.local.entity
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Represents a meaningful event spanning a time range.
*
* Events allow auto-association of images by timestamp.
*/
@Entity(
tableName = "events",
indices = [
Index(value = ["startDate"]),
Index(value = ["endDate"])
]
)
data class EventEntity(
@PrimaryKey
val eventId: String,
val name: String,
/**
* Inclusive start date (UTC millis).
*/
val startDate: Long,
/**
* Inclusive end date (UTC millis).
*/
val endDate: Long,
val location: String?,
/**
* 0.0 1.0 importance weight
*/
val importance: Float,
val isHidden: Boolean
)

View File

@@ -1,55 +0,0 @@
package com.placeholder.sherpai2.data.local.entity
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Represents a single image on the device.
*
* This entity is intentionally immutable:
* - imageUri identifies where the image lives
* - sha256 prevents duplicates
* - capturedAt is the EXIF timestamp
*
* This table should be append-only.
*/
@Entity(
tableName = "images",
indices = [
Index(value = ["imageUri"], unique = true),
Index(value = ["sha256"], unique = true),
Index(value = ["capturedAt"])
]
)
data class ImageEntity(
@PrimaryKey
val imageId: String,
val imageUri: String,
/**
* Cryptographic hash of image bytes.
* Used for deduplication and re-indexing.
*/
val sha256: String,
/**
* EXIF timestamp (UTC millis).
*/
val capturedAt: Long,
/**
* When this image was indexed into the app.
*/
val ingestedAt: Long,
val width: Int,
val height: Int,
/**
* CAMERA | SCREENSHOT | IMPORTED
*/
val source: String
)

View File

@@ -1,42 +0,0 @@
package com.placeholder.sherpai2.data.local.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
@Entity(
tableName = "image_events",
primaryKeys = ["imageId", "eventId"],
foreignKeys = [
ForeignKey(
entity = ImageEntity::class,
parentColumns = ["imageId"],
childColumns = ["imageId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = EventEntity::class,
parentColumns = ["eventId"],
childColumns = ["eventId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index("eventId")
]
)
data class ImageEventEntity(
val imageId: String,
val eventId: String,
/**
* AUTO | MANUAL
*/
val source: String,
/**
* User override flag.
*/
val override: Boolean
)

View File

@@ -1,40 +0,0 @@
package com.placeholder.sherpai2.data.local.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
@Entity(
tableName = "image_persons",
primaryKeys = ["imageId", "personId"],
foreignKeys = [
ForeignKey(
entity = ImageEntity::class,
parentColumns = ["imageId"],
childColumns = ["imageId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = PersonEntity::class,
parentColumns = ["personId"],
childColumns = ["personId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index("personId")
]
)
data class ImagePersonEntity(
val imageId: String,
val personId: String,
val confidence: Float,
val confirmed: Boolean,
/**
* PUBLIC | PRIVATE
*/
val visibility: String
)

View File

@@ -1,56 +0,0 @@
package com.placeholder.sherpai2.data.local.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
/**
* Join table linking images to tags.
*
* This is NOT optional.
* Do not inline tag lists on ImageEntity.
*/
@Entity(
tableName = "image_tags",
primaryKeys = ["imageId", "tagId"],
foreignKeys = [
ForeignKey(
entity = ImageEntity::class,
parentColumns = ["imageId"],
childColumns = ["imageId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = TagEntity::class,
parentColumns = ["tagId"],
childColumns = ["tagId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index("tagId"),
Index("imageId")
]
)
data class ImageTagEntity(
val imageId: String,
val tagId: String,
/**
* AUTO | MANUAL
*/
val source: String,
/**
* ML confidence (01).
*/
val confidence: Float,
/**
* PUBLIC | PRIVATE | HIDDEN
*/
val visibility: String,
val createdAt: Long
)

View File

@@ -1,30 +0,0 @@
package com.placeholder.sherpai2.data.local.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Represents a known person.
*
* People are separate from generic tags because:
* - face embeddings
* - privacy rules
* - identity merging
*/
@Entity(tableName = "persons")
data class PersonEntity(
@PrimaryKey
val personId: String,
val displayName: String,
/**
* Reference to face embedding storage (ML layer).
*/
val faceEmbeddingId: String?,
val isHidden: Boolean,
val createdAt: Long
)

View File

@@ -1,30 +0,0 @@
package com.placeholder.sherpai2.data.local.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Represents a conceptual tag.
*
* Tags are normalized so that:
* - "park" exists once
* - many images can reference it
*/
@Entity(tableName = "tags")
data class TagEntity(
@PrimaryKey
val tagId: String,
/**
* GENERIC | SYSTEM | HIDDEN
*/
val type: String,
/**
* Human-readable value, e.g. "park", "sunset"
*/
val value: String,
val createdAt: Long
)

View File

@@ -1,29 +0,0 @@
package com.placeholder.sherpai2.data.local.model
import androidx.room.Embedded
import androidx.room.Relation
import com.placeholder.sherpai2.data.local.entity.*
data class ImageWithEverything(
@Embedded
val image: ImageEntity,
@Relation(
parentColumn = "imageId",
entityColumn = "imageId"
)
val tags: List<ImageTagEntity>,
@Relation(
parentColumn = "imageId",
entityColumn = "imageId"
)
val persons: List<ImagePersonEntity>,
@Relation(
parentColumn = "imageId",
entityColumn = "imageId"
)
val events: List<ImageEventEntity>
)

View File

@@ -1,18 +0,0 @@
package com.placeholder.sherpai2.data.local.model
import androidx.room.Embedded
import androidx.room.Relation
import com.placeholder.sherpai2.data.local.entity.ImageEntity
import com.placeholder.sherpai2.data.local.entity.ImageTagEntity
data class ImageWithTags(
@Embedded
val image: ImageEntity,
@Relation(
parentColumn = "imageId",
entityColumn = "imageId"
)
val tags: List<ImageTagEntity>
)

View File

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

View File

@@ -0,0 +1,25 @@
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

@@ -1,50 +1,11 @@
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 kotlinx.coroutines.Dispatchers import com.placeholder.sherpai2.data.photos.SamplePhotoSource
import kotlinx.coroutines.withContext
import java.io.File
class PhotoRepository(private val context: Context) { class PhotoRepository {
fun scanExternalStorage(): Result<List<Photo>> { fun getPhotos(): List<Photo> {
// Best Practice: Use Environment.getExternalStorageDirectory() return SamplePhotoSource.loadPhotos()
// 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()
)
}
private fun isImageFile(ext: String) = listOf("jpg", "jpeg", "png").contains(ext.lowercase())
}

View File

@@ -1,28 +0,0 @@
package com.placeholder.sherpai2.di
import android.content.Context
import androidx.room.Room
import com.placeholder.sherpai2.data.local.AppDatabase
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 DatabaseModule {
@Provides
@Singleton
fun provideDatabase(
@ApplicationContext context: Context
): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"sherpai.db"
).build()
}
}

View File

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

View File

@@ -1,7 +0,0 @@
package com.placeholder.sherpai2.domain
import android.content.Context
class PhotoDuplicateScanner(private val context: Context) {
}

View File

@@ -1,5 +1,5 @@
// In navigation/AppDestinations.kt // In navigation/AppDestinations.kt
package com.placeholder.sherpai2.ui.navigation package com.placeholder.sherpai2.navigation
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*

View File

@@ -1,17 +1,13 @@
// In presentation/MainScreen.kt // In presentation/MainScreen.kt
package com.placeholder.sherpai2.presentation package com.placeholder.sherpai2.presentation
import GalleryViewModel
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel import com.placeholder.sherpai2.navigation.AppDestinations
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 import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -22,7 +18,6 @@ fun MainScreen() {
// State to track which screen is currently visible // State to track which screen is currently visible
// var currentScreen by remember { mutableStateOf(AppDestinations.Search) } // var currentScreen by remember { mutableStateOf(AppDestinations.Search) }
var currentScreen: AppDestinations by remember { mutableStateOf(AppDestinations.Search) } var currentScreen: AppDestinations by remember { mutableStateOf(AppDestinations.Search) }
val galleryViewModel: GalleryViewModel = viewModel()
// ModalNavigationDrawer provides the left sidebar UI/UX // ModalNavigationDrawer provides the left sidebar UI/UX
ModalNavigationDrawer( ModalNavigationDrawer(
@@ -55,7 +50,6 @@ fun MainScreen() {
// Displays the content for the currently selected screen // Displays the content for the currently selected screen
MainContentArea( MainContentArea(
currentScreen = currentScreen, currentScreen = currentScreen,
galleryViewModel = galleryViewModel,
modifier = Modifier.padding(paddingValues) modifier = Modifier.padding(paddingValues)
) )
} }

View File

@@ -1,14 +1,14 @@
// In presentation/AppDrawerContent.kt // In presentation/AppDrawerContent.kt
package com.placeholder.sherpai2.ui.presentation package com.placeholder.sherpai2.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.ui.navigation.AppDestinations import com.placeholder.sherpai2.navigation.AppDestinations
import com.placeholder.sherpai2.ui.navigation.mainDrawerItems import com.placeholder.sherpai2.navigation.mainDrawerItems
import com.placeholder.sherpai2.ui.navigation.utilityDrawerItems import com.placeholder.sherpai2.navigation.utilityDrawerItems
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View File

@@ -0,0 +1,104 @@
// 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,4 +1,4 @@
package com.placeholder.sherpai2.ui.presentation package com.placeholder.sherpai2.presentation
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import com.placeholder.sherpai2.data.photos.Photo import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.data.photos.SamplePhotoSource
@Composable @Composable
fun PhotoListScreen( fun PhotoListScreen(

View File

@@ -1,56 +0,0 @@
// In presentation/MainContentArea.kt
package com.placeholder.sherpai2.ui.presentation
import GalleryScreen
import GalleryViewModel
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.runtime.collectAsState
import androidx.compose.runtime.getValue
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.ui.navigation.AppDestinations
import com.placeholder.sherpai2.ui.theme.SherpAI2Theme
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MainContentArea(currentScreen: AppDestinations, modifier: Modifier = Modifier, galleryViewModel: GalleryViewModel = viewModel() ) {
val uiState by galleryViewModel.uiState.collectAsState()
Box(
modifier = modifier
.fillMaxSize()
.background(Color.Red),
contentAlignment = Alignment.Center
) {
// Swaps the UI content based on the selected screen from the drawer
when (currentScreen) {
AppDestinations.Tour -> GalleryScreen(state = uiState, modifier = Modifier)
AppDestinations.Search -> SimplePlaceholder("Find Any Photos.")
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).background(color = Color.Magenta)
)
}

View File

@@ -1,73 +1,141 @@
import androidx.compose.foundation.layout.Arrangement package com.placeholder.sherpai2.ui.tourscreen
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material3.Card
import androidx.compose.foundation.lazy.grid.items import androidx.compose.material3.CardDefaults
import androidx.compose.material3.* import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.placeholder.sherpai2.data.photos.Photo import com.placeholder.sherpai2.presentation.AlbumPreview
import com.placeholder.sherpai2.ui.tourscreen.GalleryUiState
class GalleryScreen {
}
@Composable @Composable
fun GalleryScreen( fun GalleryContent(topAlbums: List<Album>) {
state: GalleryUiState, Column(modifier = Modifier.fillMaxSize()) {
modifier: Modifier = Modifier // Add default modifier // --- Top 40% Section ---
Row(
modifier = Modifier
.fillMaxWidth()
.weight(0.4f)
.padding(8.dp)
) { ) {
// Note: If this is inside MainContentArea, you might not need a second Scaffold. // We use .getOrNull to safely check if the albums exist in the list
// Let's use a Column or Box to ensure it fills the space correctly. val firstAlbum = topAlbums.getOrNull(0)
Column( val secondAlbum = topAlbums.getOrNull(1)
modifier = modifier.fillMaxSize()
) { if (firstAlbum != null) {
Text( AlbumPreviewBoxRandom(album = firstAlbum, modifier = Modifier.weight(1f))
text = "Photo Gallery",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(16.dp)
)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (state) {
is GalleryUiState.Loading -> CircularProgressIndicator()
is GalleryUiState.Error -> Text(text = state.message)
is GalleryUiState.Success -> {
if (state.photos.isEmpty()) {
Text("No photos found. Try adding some to the emulator!")
} else { } else {
LazyVerticalGrid( Box(modifier = Modifier.weight(1f)) // Empty placeholder
columns = GridCells.Fixed(3), }
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(2.dp), if (secondAlbum != null) {
verticalArrangement = Arrangement.spacedBy(2.dp) 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)
) { ) {
items(state.photos) { photo -> Text("Lower Content Area", modifier = Modifier.align(Alignment.Center))
PhotoItem(photo)
}
}
}
}
}
} }
} }
} }
// --- STATEFUL (Use this for Production) ---
@Composable @Composable
fun PhotoItem(photo: Photo) { 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( AsyncImage(
model = photo.uri, model = photo.uri,
contentDescription = photo.title, contentDescription = null,
modifier = Modifier modifier = Modifier
.aspectRatio(1f) // Makes it a square .weight(1f)
.fillMaxWidth(), .fillMaxHeight(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop // Fills the space nicely
) )
} }
}
}
}

View File

@@ -1,9 +0,0 @@
package com.placeholder.sherpai2.ui.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

@@ -1,34 +1,96 @@
import android.app.Application package com.placeholder.sherpai2.ui.tourscreen
import androidx.lifecycle.AndroidViewModel
import android.os.Environment
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel 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 androidx.lifecycle.viewModelScope
import com.placeholder.sherpai2.data.photos.Album // Import your data model
import com.placeholder.sherpai2.data.photos.Photo import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.data.repo.PhotoRepository import kotlinx.coroutines.Dispatchers
import com.placeholder.sherpai2.ui.tourscreen.GalleryUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class GalleryViewModel(application: Application) : AndroidViewModel(application) { class GalleryViewModel : ViewModel() {
// This is the 'albums' reference your UI is looking for
// Initialize repository with the application context private val _albums = mutableStateOf<List<Album>>(emptyList())
private val repository = PhotoRepository(application) val albums: State<List<Album>> = _albums
private val _uiState = MutableStateFlow<GalleryUiState>(GalleryUiState.Loading)
val uiState = _uiState.asStateFlow()
init { init {
loadPhotos() fetchInitialData()
} }
fun loadPhotos() { private fun fetchInitialData() {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val result = repository.scanExternalStorage() val directory = File("/sdcard/photos") // Or: Environment.getExternalStorageDirectory()
result.onSuccess { photos -> if (directory.exists() && directory.isDirectory) {
_uiState.value = GalleryUiState.Success(photos) val photoFiles = directory.listFiles { file ->
}.onFailure { error -> file.extension.lowercase() in listOf("jpg", "jpeg", "png")
_uiState.value = GalleryUiState.Error(error.message ?: "Unknown Error") } ?: 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

@@ -3,5 +3,4 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
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
alias(libs.plugins.ksp) apply false
} }

View File

@@ -1,62 +1,32 @@
[versions] [versions]
# Tooling agp = "8.13.1"
agp = "8.7.3"
kotlin = "2.0.21" kotlin = "2.0.21"
ksp = "2.0.21-1.0.28" coreKtx = "1.17.0"
junit = "4.13.2"
# AndroidX / Lifecycle junitVersion = "1.3.0"
coreKtx = "1.15.0" espressoCore = "3.7.0"
lifecycle = "2.8.7" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.9.3" activityCompose = "1.12.1"
composeBom = "2024.12.01" composeBom = "2024.09.00"
navigationCompose = "2.8.5"
hiltNavigationCompose = "1.2.0"
# DI
hilt = "2.52"
# Images
coil = "2.7.0"
#backend2
#Room
room = "2.6.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } 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-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
# Compose BOM & UI
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } # ADDED THIS 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-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-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
# Navigation & Hilt
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
# Misc
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
#backend2
#Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
[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" }

View File

@@ -1,6 +1,6 @@
#Tue Dec 09 23:28:28 EST 2025 #Tue Dec 09 23:28:28 EST 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists