Compare commits
1 Commits
composeTes
...
cleanup_co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e388e9a64 |
@@ -23,5 +23,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,30 +1,70 @@
|
|||||||
package com.placeholder.sherpai2
|
package com.placeholder.sherpai2
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.placeholder.sherpai2.presentation.MainScreen // IMPORT your main screen
|
import com.placeholder.sherpai2.presentation.MainScreen // IMPORT your main screen
|
||||||
|
import com.placeholder.sherpai2.ui.theme.SherpAI2Theme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// 1. Define the permission needed based on API level
|
||||||
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
|
} else {
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
setContent {
|
setContent {
|
||||||
// Assume you have a Theme file named SherpAI2Theme (standard for new projects)
|
SherpAI2Theme {
|
||||||
// Replace with your actual project theme if different
|
// 2. State to track if permission is granted
|
||||||
MaterialTheme {
|
var hasPermission by remember {
|
||||||
Surface(
|
mutableStateOf(ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED)
|
||||||
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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package com.placeholder.sherpai2.data.photos
|
package com.placeholder.sherpai2.data.photos
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
data class Photo(
|
data class Photo(
|
||||||
val id: String,
|
val id: Long,
|
||||||
val uri: String,
|
val uri: Uri,
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
val timestamp: Long
|
val size: Long,
|
||||||
|
val dateModified: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Album(
|
data class Album(
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +1,50 @@
|
|||||||
package com.placeholder.sherpai2.data.repo
|
package com.placeholder.sherpai2.data.repo
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.util.Log
|
||||||
import com.placeholder.sherpai2.data.photos.Photo
|
import com.placeholder.sherpai2.data.photos.Photo
|
||||||
import 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<Photo> {
|
fun scanExternalStorage(): Result<List<Photo>> {
|
||||||
return SamplePhotoSource.loadPhotos()
|
// 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,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,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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// In navigation/AppDestinations.kt
|
// 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.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
// In presentation/MainScreen.kt
|
// In presentation/MainScreen.kt
|
||||||
package com.placeholder.sherpai2.presentation
|
package com.placeholder.sherpai2.presentation
|
||||||
|
|
||||||
|
import GalleryViewModel
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import 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
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -18,6 +22,7 @@ fun MainScreen() {
|
|||||||
// State to track which screen is currently visible
|
// State to track which screen is currently visible
|
||||||
// var currentScreen by remember { mutableStateOf(AppDestinations.Search) }
|
// var currentScreen by remember { mutableStateOf(AppDestinations.Search) }
|
||||||
var currentScreen: AppDestinations by remember { mutableStateOf(AppDestinations.Search) }
|
var currentScreen: AppDestinations by remember { mutableStateOf(AppDestinations.Search) }
|
||||||
|
val galleryViewModel: GalleryViewModel = viewModel()
|
||||||
|
|
||||||
// ModalNavigationDrawer provides the left sidebar UI/UX
|
// ModalNavigationDrawer provides the left sidebar UI/UX
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
@@ -50,6 +55,7 @@ fun MainScreen() {
|
|||||||
// Displays the content for the currently selected screen
|
// Displays the content for the currently selected screen
|
||||||
MainContentArea(
|
MainContentArea(
|
||||||
currentScreen = currentScreen,
|
currentScreen = currentScreen,
|
||||||
|
galleryViewModel = galleryViewModel,
|
||||||
modifier = Modifier.padding(paddingValues)
|
modifier = Modifier.padding(paddingValues)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
// In presentation/AppDrawerContent.kt
|
// In presentation/AppDrawerContent.kt
|
||||||
package com.placeholder.sherpai2.presentation
|
package com.placeholder.sherpai2.ui.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.placeholder.sherpai2.navigation.AppDestinations
|
import com.placeholder.sherpai2.ui.navigation.AppDestinations
|
||||||
import com.placeholder.sherpai2.navigation.mainDrawerItems
|
import com.placeholder.sherpai2.ui.navigation.mainDrawerItems
|
||||||
import com.placeholder.sherpai2.navigation.utilityDrawerItems
|
import com.placeholder.sherpai2.ui.navigation.utilityDrawerItems
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.placeholder.sherpai2.presentation
|
package com.placeholder.sherpai2.ui.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -11,7 +11,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
|
||||||
import com.placeholder.sherpai2.data.photos.Photo
|
import com.placeholder.sherpai2.data.photos.Photo
|
||||||
import com.placeholder.sherpai2.data.photos.SamplePhotoSource
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PhotoListScreen(
|
fun PhotoListScreen(
|
||||||
@@ -1,141 +1,73 @@
|
|||||||
package com.placeholder.sherpai2.ui.tourscreen
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.placeholder.sherpai2.presentation.AlbumPreview
|
import com.placeholder.sherpai2.data.photos.Photo
|
||||||
|
import com.placeholder.sherpai2.ui.tourscreen.GalleryUiState
|
||||||
|
|
||||||
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
|
@Composable
|
||||||
fun AlbumPreviewBox(
|
fun GalleryScreen(
|
||||||
album: Album,
|
state: GalleryUiState,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier // Add default modifier
|
||||||
) {
|
) {
|
||||||
Card(
|
// Note: If this is inside MainContentArea, you might not need a second Scaffold.
|
||||||
modifier = modifier
|
// Let's use a Column or Box to ensure it fills the space correctly.
|
||||||
.padding(8.dp)
|
Column(
|
||||||
.aspectRatio(1f), // Keeps it square
|
modifier = modifier.fillMaxSize()
|
||||||
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(
|
||||||
text = album.title,
|
text = "Photo Gallery",
|
||||||
modifier = Modifier
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
.fillMaxWidth()
|
modifier = Modifier.padding(16.dp)
|
||||||
.background(Color.Black.copy(alpha = 0.5f))
|
|
||||||
.padding(8.dp),
|
|
||||||
color = Color.White,
|
|
||||||
style = MaterialTheme.typography.labelLarge
|
|
||||||
)
|
)
|
||||||
}
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
}
|
when (state) {
|
||||||
}
|
is GalleryUiState.Loading -> CircularProgressIndicator()
|
||||||
|
is GalleryUiState.Error -> Text(text = state.message)
|
||||||
@Composable
|
is GalleryUiState.Success -> {
|
||||||
fun AlbumPreviewBoxRandom(album: Album, modifier: Modifier = Modifier) {
|
if (state.photos.isEmpty()) {
|
||||||
Card(
|
Text("No photos found. Try adding some to the emulator!")
|
||||||
modifier = modifier.padding(8.dp).aspectRatio(1f),
|
} else {
|
||||||
shape = RoundedCornerShape(12.dp)
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxSize()) {
|
items(state.photos) { photo ->
|
||||||
// Loop through the 3 random photos in the album
|
PhotoItem(photo)
|
||||||
album.photos.forEach { photo ->
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Composable
|
||||||
|
fun PhotoItem(photo: Photo) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = photo.uri,
|
model = photo.uri,
|
||||||
contentDescription = null,
|
contentDescription = photo.title,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.aspectRatio(1f) // Makes it a square
|
||||||
.fillMaxHeight(),
|
.fillMaxWidth(),
|
||||||
contentScale = ContentScale.Crop // Fills the space nicely
|
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()
|
||||||
|
}
|
||||||
@@ -1,96 +1,34 @@
|
|||||||
package com.placeholder.sherpai2.ui.tourscreen
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import android.os.Environment
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.placeholder.sherpai2.data.photos.AlbumPhoto
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.placeholder.sherpai2.data.photos.Album // Import your data model
|
|
||||||
import com.placeholder.sherpai2.data.photos.Photo
|
import com.placeholder.sherpai2.data.photos.Photo
|
||||||
import 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.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class GalleryViewModel : ViewModel() {
|
class GalleryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
// This is the 'albums' reference your UI is looking for
|
|
||||||
private val _albums = mutableStateOf<List<Album>>(emptyList())
|
// Initialize repository with the application context
|
||||||
val albums: State<List<Album>> = _albums
|
private val repository = PhotoRepository(application)
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<GalleryUiState>(GalleryUiState.Loading)
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fetchInitialData()
|
loadPhotos()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchInitialData() {
|
fun loadPhotos() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
val directory = File("/sdcard/photos") // Or: Environment.getExternalStorageDirectory()
|
val result = repository.scanExternalStorage()
|
||||||
|
|
||||||
if (directory.exists() && directory.isDirectory) {
|
result.onSuccess { photos ->
|
||||||
val photoFiles = directory.listFiles { file ->
|
_uiState.value = GalleryUiState.Success(photos)
|
||||||
file.extension.lowercase() in listOf("jpg", "jpeg", "png")
|
}.onFailure { error ->
|
||||||
} ?: emptyArray()
|
_uiState.value = GalleryUiState.Error(error.message ?: "Unknown Error")
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user