3 Commits

Author SHA1 Message Date
genki
c10cbf373f Working Gallery and Repo - Earlydays! 2025-12-20 18:27:09 -05:00
genki
91f6327c31 CheckPoint save for adding 'Tour' screen, and PhotoData and PhotoViewModels 2025-12-20 18:27:09 -05:00
genki
52fa755a3f Working Gallery and Repo - Earlydays! 2025-12-20 17:57:01 -05:00
20 changed files with 342 additions and 29 deletions

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
SherpAI2

1
.idea/gradle.xml generated
View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

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

@@ -23,5 +23,6 @@
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
</manifest>

View File

@@ -1,29 +1,70 @@
package com.placeholder.sherpai2
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import com.placeholder.sherpai2.presentation.MainScreen // IMPORT your main screen
import com.placeholder.sherpai2.ui.theme.SherpAI2Theme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. Define the permission needed based on API level
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
setContent {
// Assume you have a Theme file named SherpAI2Theme (standard for new projects)
// Replace with your actual project theme if different
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Launch the main navigation UI
MainScreen()
SherpAI2Theme {
// 2. State to track if permission is granted
var hasPermission by remember {
mutableStateOf(ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED)
}
// 3. Launcher to ask for permission
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
hasPermission = isGranted
}
// 4. Trigger request on start
LaunchedEffect(Unit) {
if (!hasPermission) launcher.launch(permission)
}
if (hasPermission) {
MainScreen() // Your existing screen that holds MainContentArea
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Please grant storage permission to view photos.")
}
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
package com.placeholder.sherpai2.data.photos
import android.net.Uri
data class Photo(
val id: Long,
val uri: Uri,
val title: String? = null,
val size: Long,
val dateModified: Int
)
data class Album(
val id: String,
val title: String,
val photos: List<Photo>
)
data class AlbumPhoto(
val id: Int,
val imageUrl: String,
val description: String
)

View File

@@ -0,0 +1,50 @@
package com.placeholder.sherpai2.data.repo
import android.content.Context
import android.provider.MediaStore
import android.content.ContentUris
import android.net.Uri
import android.os.Environment
import android.util.Log
import com.placeholder.sherpai2.data.photos.Photo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
class PhotoRepository(private val context: Context) {
fun scanExternalStorage(): Result<List<Photo>> {
// Best Practice: Use Environment.getExternalStorageDirectory()
// only as a fallback or starting point for legacy support.
val rootPath = Environment.getExternalStorageDirectory()
return runCatching {
val photos = mutableListOf<Photo>()
if (rootPath.exists() && rootPath.isDirectory) {
// walkTopDown is efficient but can throw AccessDeniedException
rootPath.walkTopDown()
.maxDepth(3) // Performance Best Practice: Don't scan the whole phone
.filter { it.isFile && isImageFile(it.extension) }
.forEach { file ->
photos.add(mapFileToPhoto(file))
}
}
photos
}.onFailure { e ->
Log.e("PhotoRepo", "Failed to scan filesystem", e)
}
}
private fun mapFileToPhoto(file: File): Photo {
return Photo(
id = file.path.hashCode().toLong(),
uri = Uri.fromFile(file),
title = file.name,
size = file.length(),
dateModified = (file.lastModified() / 1000).toInt()
)
}
private fun isImageFile(ext: String) = listOf("jpg", "jpeg", "png").contains(ext.lowercase())
}

View File

@@ -0,0 +1,3 @@
package com.placeholder.sherpai2.domain
//fun getAllPhotos(context: Context): List<Photo> {

View File

@@ -0,0 +1,7 @@
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.navigation
package com.placeholder.sherpai2.ui.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
@@ -10,6 +10,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
*/
sealed class AppDestinations(val route: String, val icon: ImageVector, val label: String) {
// Core Functional Sections
object Tour : AppDestinations(
route = "Tour",
icon = Icons.Default.PhotoLibrary,
label = "Tour"
)
object Search : AppDestinations("search", Icons.Default.Search, "Search")
object Models : AppDestinations("models", Icons.Default.Layers, "Models")
object Inventory : AppDestinations("inv", Icons.Default.Inventory2, "Inv")
@@ -23,6 +28,7 @@ sealed class AppDestinations(val route: String, val icon: ImageVector, val label
// Lists used by the AppDrawerContent to render the menu sections easily
val mainDrawerItems = listOf(
AppDestinations.Tour,
AppDestinations.Search,
AppDestinations.Models,
AppDestinations.Inventory,

View File

@@ -1,13 +1,17 @@
// In presentation/MainScreen.kt
package com.placeholder.sherpai2.presentation
import GalleryViewModel
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.placeholder.sherpai2.navigation.AppDestinations
import androidx.lifecycle.viewmodel.compose.viewModel
import com.placeholder.sherpai2.ui.navigation.AppDestinations
import com.placeholder.sherpai2.ui.presentation.AppDrawerContent
import com.placeholder.sherpai2.ui.presentation.MainContentArea
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -16,8 +20,9 @@ fun MainScreen() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
// State to track which screen is currently visible
//var currentScreen by remember { mutableStateOf(AppDestinations.Search) }
// var currentScreen by remember { mutableStateOf(AppDestinations.Search) }
var currentScreen: AppDestinations by remember { mutableStateOf(AppDestinations.Search) }
val galleryViewModel: GalleryViewModel = viewModel()
// ModalNavigationDrawer provides the left sidebar UI/UX
ModalNavigationDrawer(
@@ -50,6 +55,7 @@ fun MainScreen() {
// Displays the content for the currently selected screen
MainContentArea(
currentScreen = currentScreen,
galleryViewModel = galleryViewModel,
modifier = Modifier.padding(paddingValues)
)
}

View File

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

View File

@@ -1,27 +1,38 @@
// In presentation/MainContentArea.kt
package com.placeholder.sherpai2.presentation
package com.placeholder.sherpai2.ui.presentation
import GalleryScreen
import GalleryViewModel
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.placeholder.sherpai2.navigation.AppDestinations
import com.placeholder.sherpai2.ui.navigation.AppDestinations
import com.placeholder.sherpai2.ui.theme.SherpAI2Theme
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MainContentArea(currentScreen: AppDestinations, modifier: Modifier = Modifier) {
fun MainContentArea(currentScreen: AppDestinations, modifier: Modifier = Modifier, galleryViewModel: GalleryViewModel = viewModel() ) {
val uiState by galleryViewModel.uiState.collectAsState()
Box(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant),
.background(Color.Red),
contentAlignment = Alignment.Center
) {
// Swaps the UI content based on the selected screen from the drawer
when (currentScreen) {
AppDestinations.Search -> SimplePlaceholder("Search Screen: Find your models and data.")
AppDestinations.Tour -> GalleryScreen(state = uiState, modifier = Modifier)
AppDestinations.Search -> SimplePlaceholder("Find Any Photos.")
AppDestinations.Models -> SimplePlaceholder("Models Screen: Manage your LoRA/embeddings.")
AppDestinations.Inventory -> SimplePlaceholder("Inventory Screen: View all collected data.")
AppDestinations.Train -> SimplePlaceholder("Train Screen: Start the LoRA adaptation process.")
@@ -37,6 +48,9 @@ private fun SimplePlaceholder(text: String) {
Text(
text = text,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp)
modifier = Modifier.padding(16.dp).background(color = Color.Magenta)
)
}

View File

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

View File

@@ -0,0 +1,73 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.ui.tourscreen.GalleryUiState
@Composable
fun GalleryScreen(
state: GalleryUiState,
modifier: Modifier = Modifier // Add default modifier
) {
// Note: If this is inside MainContentArea, you might not need a second Scaffold.
// Let's use a Column or Box to ensure it fills the space correctly.
Column(
modifier = modifier.fillMaxSize()
) {
Text(
text = "Photo Gallery",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(16.dp)
)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (state) {
is GalleryUiState.Loading -> CircularProgressIndicator()
is GalleryUiState.Error -> Text(text = state.message)
is GalleryUiState.Success -> {
if (state.photos.isEmpty()) {
Text("No photos found. Try adding some to the emulator!")
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
items(state.photos) { photo ->
PhotoItem(photo)
}
}
}
}
}
}
}
}
@Composable
fun PhotoItem(photo: Photo) {
AsyncImage(
model = photo.uri,
contentDescription = photo.title,
modifier = Modifier
.aspectRatio(1f) // Makes it a square
.fillMaxWidth(),
contentScale = ContentScale.Crop
)
}

View File

@@ -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()
}

View File

@@ -0,0 +1,35 @@
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.placeholder.sherpai2.data.photos.Photo
import com.placeholder.sherpai2.data.repo.PhotoRepository
import com.placeholder.sherpai2.ui.tourscreen.GalleryUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class GalleryViewModel(application: Application) : AndroidViewModel(application) {
// Initialize repository with the application context
private val repository = PhotoRepository(application)
private val _uiState = MutableStateFlow<GalleryUiState>(GalleryUiState.Loading)
val uiState = _uiState.asStateFlow()
init {
loadPhotos()
}
fun loadPhotos() {
viewModelScope.launch {
val result = repository.scanExternalStorage()
result.onSuccess { photos ->
_uiState.value = GalleryUiState.Success(photos)
}.onFailure { error ->
_uiState.value = GalleryUiState.Error(error.message ?: "Unknown Error")
}
}
}
}

View File

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