CheckPoint save for adding 'Tour' screen, and PhotoData and PhotoViewModels

This commit is contained in:
genki
2025-12-20 11:39:46 -05:00
parent 52fa755a3f
commit 91f6327c31
14 changed files with 409 additions and 5 deletions

View File

@@ -72,4 +72,6 @@ dependencies {
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
implementation("io.coil-kt:coil-compose:2.6.0")
}

View File

@@ -26,4 +26,5 @@ class MainActivity : ComponentActivity() {
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
package com.placeholder.sherpai2.data.photos
data class Photo(
val id: String,
val uri: String,
val title: String? = null,
val timestamp: Long
)
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
)

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

@@ -0,0 +1,11 @@
package com.placeholder.sherpai2.data.repo
import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.data.photos.SamplePhotoSource
class PhotoRepository {
fun getPhotos(): List<Photo> {
return SamplePhotoSource.loadPhotos()
}
}

View File

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

View File

@@ -16,7 +16,7 @@ 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) }
// ModalNavigationDrawer provides the left sidebar UI/UX

View File

@@ -1,15 +1,20 @@
// 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) {
@@ -21,7 +26,8 @@ fun MainContentArea(currentScreen: AppDestinations, modifier: Modifier = Modifie
) {
// 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 -> 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.")
@@ -39,4 +45,60 @@ private fun SimplePlaceholder(text: String) {
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

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

View File

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

View File

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

View File

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