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
18 changed files with 359 additions and 268 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

@@ -23,6 +23,5 @@
</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>

View File

@@ -1,70 +1,30 @@
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 {
SherpAI2Theme {
// 2. State to track if permission is granted
var hasPermission by remember {
mutableStateOf(ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED)
// 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()
}
// 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,13 +1,10 @@
package com.placeholder.sherpai2.data.photos
import android.net.Uri
data class Photo(
val id: Long,
val uri: Uri,
val id: String,
val uri: String,
val title: String? = null,
val size: Long,
val dateModified: Int
val timestamp: Long
)
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
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
import com.placeholder.sherpai2.data.photos.SamplePhotoSource
class PhotoRepository(private val context: Context) {
class PhotoRepository {
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))
fun getPhotos(): List<Photo> {
return SamplePhotoSource.loadPhotos()
}
}
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,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
package com.placeholder.sherpai2.ui.navigation
package com.placeholder.sherpai2.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*

View File

@@ -1,17 +1,13 @@
// 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 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 com.placeholder.sherpai2.navigation.AppDestinations
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -22,7 +18,6 @@ fun MainScreen() {
// State to track which screen is currently visible
// 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(
@@ -55,7 +50,6 @@ fun MainScreen() {
// Displays the content for the currently selected screen
MainContentArea(
currentScreen = currentScreen,
galleryViewModel = galleryViewModel,
modifier = Modifier.padding(paddingValues)
)
}

View File

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

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.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.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.*
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.Alignment
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.data.photos.Photo
import com.placeholder.sherpai2.ui.tourscreen.GalleryUiState
import com.placeholder.sherpai2.presentation.AlbumPreview
class GalleryScreen {
}
@Composable
fun GalleryScreen(
state: GalleryUiState,
modifier: Modifier = Modifier // Add default modifier
fun GalleryContent(topAlbums: List<Album>) {
Column(modifier = Modifier.fillMaxSize()) {
// --- 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.
// 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!")
// 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 {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
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)
) {
items(state.photos) { photo ->
PhotoItem(photo)
}
}
}
}
}
Text("Lower Content Area", modifier = Modifier.align(Alignment.Center))
}
}
}
// --- STATEFUL (Use this for Production) ---
@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(
model = photo.uri,
contentDescription = photo.title,
contentDescription = null,
modifier = Modifier
.aspectRatio(1f) // Makes it a square
.fillMaxWidth(),
contentScale = ContentScale.Crop
.weight(1f)
.fillMaxHeight(),
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
import androidx.lifecycle.AndroidViewModel
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 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.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
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()
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 {
loadPhotos()
fetchInitialData()
}
fun loadPhotos() {
viewModelScope.launch {
val result = repository.scanExternalStorage()
private fun fetchInitialData() {
viewModelScope.launch(Dispatchers.IO) {
val directory = File("/sdcard/photos") // Or: Environment.getExternalStorageDirectory()
result.onSuccess { photos ->
_uiState.value = GalleryUiState.Success(photos)
}.onFailure { error ->
_uiState.value = GalleryUiState.Error(error.message ?: "Unknown Error")
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)
}
}
}
}