Compare commits
4 Commits
composeTes
...
c458e08075
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c458e08075 | ||
|
|
c10cbf373f | ||
|
|
91f6327c31 | ||
|
|
52fa755a3f |
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
SherpAI2
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
|
||||
4
.kotlin/errors/errors-1766244784240.log
Normal file
4
.kotlin/errors/errors-1766244784240.log
Normal 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
|
||||
|
||||
@@ -1,75 +1,76 @@
|
||||
// build.gradle.kts (Module: :app)
|
||||
|
||||
plugins {
|
||||
// 1. Core Android and Kotlin plugins (MUST be first)
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
|
||||
id("org.jetbrains.kotlin.plugin.compose") // Note: No version is specified here
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt.android)
|
||||
}
|
||||
|
||||
android {
|
||||
// 2. Android Configuration
|
||||
namespace = "com.placeholder.sherpai2"
|
||||
compileSdk = 34
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.placeholder.sherpai2"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
// 3. Kotlin & Java Settings
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
// 4. Jetpack Compose Configuration (Crucial!)
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.8" // Must match your Kotlin version
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// --- CORE ANDROID & LIFECYCLE ---
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
|
||||
implementation("androidx.activity:activity-compose:1.8.2") // Fixes 'activity' ref error
|
||||
// AndroidX & Lifecycle
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
// --- JETPACK COMPOSE UI (Material 3) ---
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3") // Fixes 'material3' ref error
|
||||
// Compose (Using BOM)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons)
|
||||
|
||||
// --- COMPOSE ICONS (Fixes 'material' and 'Icons' ref errors) ---
|
||||
// Uses direct string to avoid Version Catalog conflicts
|
||||
implementation("androidx.compose.material:material-icons-extended:1.6.0")
|
||||
// Navigation & Hilt Integration
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
// --- STATE MANAGEMENT / COROUTINES ---
|
||||
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")
|
||||
// Images
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
// --- TESTING ---
|
||||
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")
|
||||
// Tooling (Fixed to use TOML alias)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
implementation("androidx.compose.foundation:foundation:1.6.0") // Use your current Compose version
|
||||
implementation("androidx.compose.material3:material3:1.2.1") // <-- Fix/Reconfirm Material 3
|
||||
//backend2
|
||||
//room addon
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
ksp(libs.room.compiler)
|
||||
}
|
||||
@@ -23,5 +23,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
</manifest>
|
||||
@@ -1,29 +1,70 @@
|
||||
package com.placeholder.sherpai2
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
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.material3.MaterialTheme
|
||||
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.core.content.ContextCompat
|
||||
import com.placeholder.sherpai2.presentation.MainScreen // IMPORT your main screen
|
||||
import com.placeholder.sherpai2.ui.theme.SherpAI2Theme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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 {
|
||||
// Assume you have a Theme file named SherpAI2Theme (standard for new projects)
|
||||
// Replace with your actual project theme if different
|
||||
MaterialTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
// Launch the main navigation UI
|
||||
MainScreen()
|
||||
SherpAI2Theme {
|
||||
// 2. State to track if permission is granted
|
||||
var hasPermission by remember {
|
||||
mutableStateOf(ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
|
||||
// 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
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 (0–1).
|
||||
*/
|
||||
val confidence: Float,
|
||||
|
||||
/**
|
||||
* PUBLIC | PRIVATE | HIDDEN
|
||||
*/
|
||||
val visibility: String,
|
||||
|
||||
val createdAt: Long
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
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>
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
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>
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.placeholder.sherpai2.data.photos
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
data class Photo(
|
||||
val id: Long,
|
||||
val uri: Uri,
|
||||
val title: String? = null,
|
||||
val size: Long,
|
||||
val dateModified: Int
|
||||
)
|
||||
|
||||
data class Album(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val photos: List<Photo>
|
||||
)
|
||||
|
||||
data class AlbumPhoto(
|
||||
val id: Int,
|
||||
val imageUrl: String,
|
||||
val description: String
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class PhotoRepository(private val context: Context) {
|
||||
|
||||
fun scanExternalStorage(): Result<List<Photo>> {
|
||||
// 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()
|
||||
)
|
||||
}
|
||||
|
||||
private fun isImageFile(ext: String) = listOf("jpg", "jpeg", "png").contains(ext.lowercase())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.placeholder.sherpai2.domain
|
||||
|
||||
//fun getAllPhotos(context: Context): List<Photo> {
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.placeholder.sherpai2.domain
|
||||
|
||||
import android.content.Context
|
||||
|
||||
class PhotoDuplicateScanner(private val context: Context) {
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// In navigation/AppDestinations.kt
|
||||
package com.placeholder.sherpai2.navigation
|
||||
package com.placeholder.sherpai2.ui.navigation
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
@@ -10,6 +10,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
*/
|
||||
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")
|
||||
@@ -23,6 +28,7 @@ sealed class AppDestinations(val route: String, val icon: ImageVector, val label
|
||||
|
||||
// Lists used by the AppDrawerContent to render the menu sections easily
|
||||
val mainDrawerItems = listOf(
|
||||
AppDestinations.Tour,
|
||||
AppDestinations.Search,
|
||||
AppDestinations.Models,
|
||||
AppDestinations.Inventory,
|
||||
@@ -1,13 +1,17 @@
|
||||
// In presentation/MainScreen.kt
|
||||
package com.placeholder.sherpai2.presentation
|
||||
|
||||
import GalleryViewModel
|
||||
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 androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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)
|
||||
@@ -16,8 +20,9 @@ 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 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(
|
||||
@@ -50,6 +55,7 @@ fun MainScreen() {
|
||||
// Displays the content for the currently selected screen
|
||||
MainContentArea(
|
||||
currentScreen = currentScreen,
|
||||
galleryViewModel = galleryViewModel,
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
// In presentation/AppDrawerContent.kt
|
||||
package com.placeholder.sherpai2.presentation
|
||||
package com.placeholder.sherpai2.ui.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.placeholder.sherpai2.navigation.AppDestinations
|
||||
import com.placeholder.sherpai2.navigation.mainDrawerItems
|
||||
import com.placeholder.sherpai2.navigation.utilityDrawerItems
|
||||
import com.placeholder.sherpai2.ui.navigation.AppDestinations
|
||||
import com.placeholder.sherpai2.ui.navigation.mainDrawerItems
|
||||
import com.placeholder.sherpai2.ui.navigation.utilityDrawerItems
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -1,27 +1,38 @@
|
||||
// In presentation/MainContentArea.kt
|
||||
package com.placeholder.sherpai2.presentation
|
||||
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.navigation.AppDestinations
|
||||
|
||||
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) {
|
||||
Box(
|
||||
fun MainContentArea(currentScreen: AppDestinations, modifier: Modifier = Modifier, galleryViewModel: GalleryViewModel = viewModel() ) {
|
||||
val uiState by galleryViewModel.uiState.collectAsState()
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
.background(Color.Red),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Swaps the UI content based on the selected screen from the drawer
|
||||
when (currentScreen) {
|
||||
AppDestinations.Search -> SimplePlaceholder("Search Screen: Find your models and data.")
|
||||
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.")
|
||||
@@ -37,6 +48,9 @@ private fun SimplePlaceholder(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
modifier = Modifier.padding(16.dp).background(color = Color.Magenta)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.placeholder.sherpai2.ui.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
|
||||
|
||||
@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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.tourscreen.GalleryUiState
|
||||
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
fun GalleryScreen(
|
||||
state: GalleryUiState,
|
||||
modifier: Modifier = Modifier // Add default modifier
|
||||
) {
|
||||
// Note: If this is inside MainContentArea, you might not need a second Scaffold.
|
||||
// Let's use a Column or Box to ensure it fills the space correctly.
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
Text(
|
||||
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 {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
items(state.photos) { 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
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.placeholder.sherpai2.data.photos.Photo
|
||||
import com.placeholder.sherpai2.data.repo.PhotoRepository
|
||||
import com.placeholder.sherpai2.ui.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.placeholder.sherpai2.ui.tourscreen.components
|
||||
|
||||
class AlbumBox {
|
||||
}
|
||||
@@ -3,4 +3,5 @@ plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
}
|
||||
@@ -1,32 +1,62 @@
|
||||
[versions]
|
||||
agp = "8.13.1"
|
||||
# Tooling
|
||||
agp = "8.7.3"
|
||||
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"
|
||||
ksp = "2.0.21-1.0.28"
|
||||
|
||||
# AndroidX / Lifecycle
|
||||
coreKtx = "1.15.0"
|
||||
lifecycle = "2.8.7"
|
||||
activityCompose = "1.9.3"
|
||||
composeBom = "2024.12.01"
|
||||
navigationCompose = "2.8.5"
|
||||
hiltNavigationCompose = "1.2.0"
|
||||
|
||||
# DI
|
||||
hilt = "2.52"
|
||||
|
||||
# Images
|
||||
coil = "2.7.0"
|
||||
|
||||
#backend2
|
||||
#Room
|
||||
room = "2.6.1"
|
||||
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
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-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
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-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 = { group = "androidx.compose.ui", name = "ui-tooling" } # ADDED THIS
|
||||
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-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]
|
||||
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" }
|
||||
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Tue Dec 09 23:28:28 EST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user