From c10cbf373fc1f80b14d9ce611371f7a2aa710e46 Mon Sep 17 00:00:00 2001 From: genki <123@1234.com> Date: Sat, 20 Dec 2025 17:54:16 -0500 Subject: [PATCH] Working Gallery and Repo - Earlydays! --- app/src/main/AndroidManifest.xml | 3 +- .../com/placeholder/sherpai2/MainActivity.kt | 60 +++++-- .../placeholder/sherpai2/data/photos/Photo.kt | 9 +- .../sherpai2/data/photos/SamplePhoto.kt | 25 --- .../sherpai2/data/repo/PhotoRepository.kt | 47 ++++- .../sherpai2/domain/PhotoFunctions.kt | 3 + .../sherpai2/domain/PhotoScanner.kt | 7 + .../sherpai2/presentation/MainContentArea.kt | 104 ----------- .../{ => ui}/navigation/AppDestinations.kt | 2 +- .../{ => ui}/navigation/MainScreen.kt | 8 +- .../{ => ui}/presentation/AppDrawerContent.kt | 8 +- .../ui/presentation/MainContentArea.kt | 56 ++++++ .../{ => ui}/presentation/PhotoListScreen.kt | 3 +- .../sherpai2/ui/tourscreen/GalleryScreen.kt | 170 ++++++------------ .../sherpai2/ui/tourscreen/GalleryUiState.kt | 9 + .../ui/tourscreen/GalleryViewModel.kt | 104 +++-------- 16 files changed, 261 insertions(+), 357 deletions(-) delete mode 100644 app/src/main/java/com/placeholder/sherpai2/data/photos/SamplePhoto.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/domain/PhotoFunctions.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/domain/PhotoScanner.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/presentation/MainContentArea.kt rename app/src/main/java/com/placeholder/sherpai2/{ => ui}/navigation/AppDestinations.kt (96%) rename app/src/main/java/com/placeholder/sherpai2/{ => ui}/navigation/MainScreen.kt (84%) rename app/src/main/java/com/placeholder/sherpai2/{ => ui}/presentation/AppDrawerContent.kt (89%) create mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainContentArea.kt rename app/src/main/java/com/placeholder/sherpai2/{ => ui}/presentation/PhotoListScreen.kt (89%) create mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryUiState.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c2a2fd7..f4f001a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,5 +23,6 @@ - + + \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/MainActivity.kt b/app/src/main/java/com/placeholder/sherpai2/MainActivity.kt index 086bf6d..3a51d89 100644 --- a/app/src/main/java/com/placeholder/sherpai2/MainActivity.kt +++ b/app/src/main/java/com/placeholder/sherpai2/MainActivity.kt @@ -1,30 +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.") + } } } + + + } } - } } + diff --git a/app/src/main/java/com/placeholder/sherpai2/data/photos/Photo.kt b/app/src/main/java/com/placeholder/sherpai2/data/photos/Photo.kt index 7c8fc22..7370424 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/photos/Photo.kt +++ b/app/src/main/java/com/placeholder/sherpai2/data/photos/Photo.kt @@ -1,10 +1,13 @@ package com.placeholder.sherpai2.data.photos +import android.net.Uri + data class Photo( - val id: String, - val uri: String, + val id: Long, + val uri: Uri, val title: String? = null, - val timestamp: Long + val size: Long, + val dateModified: Int ) data class Album( diff --git a/app/src/main/java/com/placeholder/sherpai2/data/photos/SamplePhoto.kt b/app/src/main/java/com/placeholder/sherpai2/data/photos/SamplePhoto.kt deleted file mode 100644 index 095bd2d..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/data/photos/SamplePhoto.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.placeholder.sherpai2.data.photos - -import com.placeholder.sherpai2.data.photos.Photo - -object SamplePhotoSource { - - fun loadPhotos(): List { - 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 \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/data/repo/PhotoRepository.kt b/app/src/main/java/com/placeholder/sherpai2/data/repo/PhotoRepository.kt index 41cc9f8..d6286cb 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/repo/PhotoRepository.kt +++ b/app/src/main/java/com/placeholder/sherpai2/data/repo/PhotoRepository.kt @@ -1,11 +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 com.placeholder.sherpai2.data.photos.SamplePhotoSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File -class PhotoRepository { +class PhotoRepository(private val context: Context) { - fun getPhotos(): List { - return SamplePhotoSource.loadPhotos() + fun scanExternalStorage(): Result> { + // Best Practice: Use Environment.getExternalStorageDirectory() + // only as a fallback or starting point for legacy support. + val rootPath = Environment.getExternalStorageDirectory() + + return runCatching { + val photos = mutableListOf() + + 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()) } \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/domain/PhotoFunctions.kt b/app/src/main/java/com/placeholder/sherpai2/domain/PhotoFunctions.kt new file mode 100644 index 0000000..a31556f --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/domain/PhotoFunctions.kt @@ -0,0 +1,3 @@ +package com.placeholder.sherpai2.domain + +//fun getAllPhotos(context: Context): List { diff --git a/app/src/main/java/com/placeholder/sherpai2/domain/PhotoScanner.kt b/app/src/main/java/com/placeholder/sherpai2/domain/PhotoScanner.kt new file mode 100644 index 0000000..18bf55b --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/domain/PhotoScanner.kt @@ -0,0 +1,7 @@ +package com.placeholder.sherpai2.domain + +import android.content.Context + +class PhotoDuplicateScanner(private val context: Context) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/presentation/MainContentArea.kt b/app/src/main/java/com/placeholder/sherpai2/presentation/MainContentArea.kt deleted file mode 100644 index 7602a65..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/presentation/MainContentArea.kt +++ /dev/null @@ -1,104 +0,0 @@ -// In presentation/MainContentArea.kt -package com.placeholder.sherpai2.presentation - -import androidx.compose.ui.graphics.Color -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.placeholder.sherpai2.navigation.AppDestinations -import com.placeholder.sherpai2.ui.theme.SherpAI2Theme - -@Composable -fun MainContentArea(currentScreen: AppDestinations, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceVariant), - contentAlignment = Alignment.Center - ) { - // Swaps the UI content based on the selected screen from the drawer - when (currentScreen) { - AppDestinations.Tour -> TwinAlbumScreen("New Collections." , "Classics") - AppDestinations.Search -> SimplePlaceholder("Search for your photo.") - AppDestinations.Models -> SimplePlaceholder("Models Screen: Manage your LoRA/embeddings.") - AppDestinations.Inventory -> SimplePlaceholder("Inventory Screen: View all collected data.") - AppDestinations.Train -> SimplePlaceholder("Train Screen: Start the LoRA adaptation process.") - AppDestinations.Tags -> SimplePlaceholder("Tags Screen: Create and edit custom tags.") - AppDestinations.Upload -> SimplePlaceholder("Upload Screen: Import new photos/data.") - AppDestinations.Settings -> SimplePlaceholder("Settings Screen: Configure app behavior.") - } - } -} - -@Composable -private fun SimplePlaceholder(text: String) { - Text( - text = text, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(16.dp) - ) -} - - -@Composable -fun AlbumPreview(title: String, color: Color) { - Box( - modifier = Modifier - .padding(8.dp) - .aspectRatio(1f) // Makes it a square - .background(color, shape = RoundedCornerShape(12.dp)), - contentAlignment = Alignment.Center - ) { - Text(text = title, color = Color.White, fontWeight = FontWeight.Bold) - } -} - -@Composable -fun TwinAlbumScreen(albumNameA: String, albumNameB: String) { - Column(modifier = Modifier.fillMaxSize()) { - // --- Top 40% Section --- - Row( - modifier = Modifier - .fillMaxWidth() - .weight(0.30f) // This takes exactly 40% of vertical space - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // These two occupy the 40% area - Box(modifier = Modifier.weight(1f)) { - AlbumPreview("Mackenzie Hazer", Color.Blue) - } - Box(modifier = Modifier.weight(1f)) { - AlbumPreview("Winifred", Color.Magenta) - } - } - - // --- Bottom 60% Section --- - Box( - modifier = Modifier - .fillMaxWidth() - .weight(0.6f) // This takes the remaining 60% - .background(Color.LightGray.copy(alpha = 0.2f)) - ) { - Text("Other content goes here", modifier = Modifier.align(Alignment.Center)) - } - } -} -@Preview -@Composable -fun PreviewTwinTopRow() { - SherpAI2Theme { - TwinAlbumScreen("Album A", "Album B") - } -} - - diff --git a/app/src/main/java/com/placeholder/sherpai2/navigation/AppDestinations.kt b/app/src/main/java/com/placeholder/sherpai2/ui/navigation/AppDestinations.kt similarity index 96% rename from app/src/main/java/com/placeholder/sherpai2/navigation/AppDestinations.kt rename to app/src/main/java/com/placeholder/sherpai2/ui/navigation/AppDestinations.kt index 992d2dc..ab4baf3 100644 --- a/app/src/main/java/com/placeholder/sherpai2/navigation/AppDestinations.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/navigation/AppDestinations.kt @@ -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.* diff --git a/app/src/main/java/com/placeholder/sherpai2/navigation/MainScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/navigation/MainScreen.kt similarity index 84% rename from app/src/main/java/com/placeholder/sherpai2/navigation/MainScreen.kt rename to app/src/main/java/com/placeholder/sherpai2/ui/navigation/MainScreen.kt index 5baf9c2..50ce1ce 100644 --- a/app/src/main/java/com/placeholder/sherpai2/navigation/MainScreen.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/navigation/MainScreen.kt @@ -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) @@ -18,6 +22,7 @@ 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( @@ -50,6 +55,7 @@ fun MainScreen() { // Displays the content for the currently selected screen MainContentArea( currentScreen = currentScreen, + galleryViewModel = galleryViewModel, modifier = Modifier.padding(paddingValues) ) } diff --git a/app/src/main/java/com/placeholder/sherpai2/presentation/AppDrawerContent.kt b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt similarity index 89% rename from app/src/main/java/com/placeholder/sherpai2/presentation/AppDrawerContent.kt rename to app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt index b17fa59..53d2d25 100644 --- a/app/src/main/java/com/placeholder/sherpai2/presentation/AppDrawerContent.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt @@ -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 diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainContentArea.kt b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainContentArea.kt new file mode 100644 index 0000000..51541a3 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainContentArea.kt @@ -0,0 +1,56 @@ +// 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) + ) +} + + + diff --git a/app/src/main/java/com/placeholder/sherpai2/presentation/PhotoListScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/PhotoListScreen.kt similarity index 89% rename from app/src/main/java/com/placeholder/sherpai2/presentation/PhotoListScreen.kt rename to app/src/main/java/com/placeholder/sherpai2/ui/presentation/PhotoListScreen.kt index e911100..18815ed 100644 --- a/app/src/main/java/com/placeholder/sherpai2/presentation/PhotoListScreen.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/PhotoListScreen.kt @@ -1,4 +1,4 @@ -package com.placeholder.sherpai2.presentation +package com.placeholder.sherpai2.ui.presentation import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* @@ -11,7 +11,6 @@ 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( diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryScreen.kt index 6165294..a101cff 100644 --- a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryScreen.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryScreen.kt @@ -1,141 +1,73 @@ -package com.placeholder.sherpai2.ui.tourscreen - -import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement 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.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.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.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import com.placeholder.sherpai2.presentation.AlbumPreview +import com.placeholder.sherpai2.data.photos.Photo +import com.placeholder.sherpai2.ui.tourscreen.GalleryUiState -class GalleryScreen { -} -@Composable -fun GalleryContent(topAlbums: List) { - 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 +fun GalleryScreen( + state: GalleryUiState, + modifier: Modifier = Modifier // Add default modifier ) { - Card( - modifier = modifier - .padding(8.dp) - .aspectRatio(1f), // Keeps it square - shape = RoundedCornerShape(12.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.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() ) { - 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 - ) + 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 + ) } \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryUiState.kt b/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryUiState.kt new file mode 100644 index 0000000..78f8dc2 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryUiState.kt @@ -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) : GalleryUiState() + data class Error(val message: String) : GalleryUiState() +} diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryViewModel.kt b/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryViewModel.kt index b18fb68..c521a17 100644 --- a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryViewModel.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryViewModel.kt @@ -1,96 +1,34 @@ -package com.placeholder.sherpai2.ui.tourscreen - -import android.os.Environment -import androidx.compose.runtime.mutableStateOf +import android.app.Application +import androidx.lifecycle.AndroidViewModel 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 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 -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>(emptyList()) - val albums: State> = _albums +class GalleryViewModel(application: Application) : AndroidViewModel(application) { + + // Initialize repository with the application context + private val repository = PhotoRepository(application) + + private val _uiState = MutableStateFlow(GalleryUiState.Loading) + val uiState = _uiState.asStateFlow() init { - fetchInitialData() + loadPhotos() } - private fun fetchInitialData() { - viewModelScope.launch(Dispatchers.IO) { - val directory = File("/sdcard/photos") // Or: Environment.getExternalStorageDirectory() + fun loadPhotos() { + viewModelScope.launch { + val result = repository.scanExternalStorage() - 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) - } + result.onSuccess { photos -> + _uiState.value = GalleryUiState.Success(photos) + }.onFailure { error -> + _uiState.value = GalleryUiState.Error(error.message ?: "Unknown Error") } } }