From 0d34a2510b4f795ed854b51f6c57d2737b1926d0 Mon Sep 17 00:00:00 2001 From: genki <123@1234.com> Date: Thu, 25 Dec 2025 00:40:57 -0500 Subject: [PATCH] Mess - Crash on boot - Backend ?? --- app/build.gradle.kts | 10 +- app/src/main/AndroidManifest.xml | 3 +- .../com/placeholder/sherpai2/MainActivity.kt | 107 +++++++++++++----- .../sherpai2/SherpAIApplication.kt | 7 ++ .../data/local/dao/ImageAggregateDao.kt | 27 ++++- .../sherpai2/data/local/dao/ImageTagDao.kt | 12 ++ .../sherpai2/di/RepositoryModule.kt | 28 +++++ .../domain/repository/ImageRepository.kt | 31 +++++ .../domain/repository/ImageRepositoryImpl.kt | 64 +++++++++++ .../domain/repository/TaggingRepository.kt | 30 +++++ .../repository/TaggingRepositoryImpl.kt | 97 ++++++++++++++++ .../ui/imagedetail/ImageDetailScreen.kt | 86 ++++++++++++++ .../viewmodel/ImageDetailViewModel.kt | 55 +++++++++ .../sherpai2/ui/navigation/AppDestinations.kt | 71 +++++++++--- .../sherpai2/ui/navigation/MainScreen.kt | 63 ----------- .../ui/presentation/AppDrawerContent.kt | 59 ---------- .../ui/presentation/MainContentArea.kt | 56 --------- .../ui/presentation/PhotoListScreen.kt | 34 ------ .../sherpai2/ui/search/SearchScreen.kt | 70 ++++++++++++ .../sherpai2/ui/search/SearchViewModel.kt | 24 ++++ .../ui/search/components/ImageGridItem.kt | 28 +++++ .../sherpai2/ui/tourscreen/GalleryScreen.kt | 73 ------------ .../sherpai2/ui/tourscreen/GalleryUiState.kt | 9 -- .../ui/tourscreen/GalleryViewModel.kt | 35 ------ .../ui/tourscreen/components/AlbumBox.kt | 4 - gradle/libs.versions.toml | 4 + 26 files changed, 708 insertions(+), 379 deletions(-) create mode 100644 app/src/main/java/com/placeholder/sherpai2/SherpAIApplication.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/di/RepositoryModule.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/domain/repository/ImageRepository.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/domain/repository/ImageRepositoryImpl.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/domain/repository/TaggingRepository.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/domain/repository/TaggingRepositoryImpl.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/imagedetail/ImageDetailScreen.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/imagedetail/viewmodel/ImageDetailViewModel.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/navigation/MainScreen.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainContentArea.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/presentation/PhotoListScreen.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/search/SearchScreen.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/search/SearchViewModel.kt create mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/search/components/ImageGridItem.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryScreen.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryUiState.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryViewModel.kt delete mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/components/AlbumBox.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c831952..b2779c1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,12 +12,18 @@ android { defaultConfig { applicationId = "com.placeholder.sherpai2" - minSdk = 24 + minSdk = 25 targetSdk = 35 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + javaCompileOptions { + annotationProcessorOptions { + // Correct Kotlin DSL syntax: use put() instead of += with a map literal + arguments["room.schemaLocation"] = "$projectDir/schemas" + } + } } buildTypes { @@ -60,6 +66,8 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.hilt.android) + implementation(libs.compose.material3) + implementation(libs.androidx.material3) ksp(libs.hilt.compiler) // Images diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f4f001a..ce43118 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,8 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.SherpAI2"> + android:theme="@style/Theme.SherpAI2" + android:name=".SherpAIApplication"> = Build.VERSION_CODES.TIRAMISU) { + val mediaPermission = 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) + mutableStateOf( + ContextCompat.checkSelfPermission(this, mediaPermission) == + PackageManager.PERMISSION_GRANTED + ) } - // 3. Launcher to ask for permission - val launcher = rememberLauncherForActivityResult( + val permissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() - ) { isGranted -> - hasPermission = isGranted - } + ) { granted -> hasPermission = granted } - // 4. Trigger request on start LaunchedEffect(Unit) { - if (!hasPermission) launcher.launch(permission) + if (!hasPermission) permissionLauncher.launch(mediaPermission) } if (hasPermission) { - MainScreen() // Your existing screen that holds MainContentArea + AppNavigation() } else { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Please grant storage permission to view photos.") - } + PermissionDeniedScreen() } } - - - } } + } } +@Composable +fun AppNavigation() { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = Routes.Search + ) { + + // Search screen + composable(Routes.Search) { + val searchViewModel: SearchViewModel = hiltViewModel() + + SearchScreen( + searchViewModel = searchViewModel, + onImageClick = { imageId -> + navController.navigate("${Routes.ImageDetail}/$imageId") + } + ) + } + + // Image detail screen + composable( + route = "${Routes.ImageDetail}/{imageId}", + arguments = listOf(navArgument("imageId") { type = NavType.StringType }) + ) { backStackEntry -> + val imageId = backStackEntry.arguments?.getString("imageId") ?: "" + val detailViewModel: ImageDetailViewModel = hiltViewModel() + + ImageDetailScreen( + imageUri = imageId, + onBack = { navController.popBackStack() } + ) + } + } +} + +@Composable +private fun PermissionDeniedScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Please grant photo access to use the app.") + } +} diff --git a/app/src/main/java/com/placeholder/sherpai2/SherpAIApplication.kt b/app/src/main/java/com/placeholder/sherpai2/SherpAIApplication.kt new file mode 100644 index 0000000..6e2417b --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/SherpAIApplication.kt @@ -0,0 +1,7 @@ +package com.placeholder.sherpai2 + +import android.app.Application + +class SherpAIApplication : Application() { + +} diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageAggregateDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageAggregateDao.kt index 4f1841a..956b862 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageAggregateDao.kt +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageAggregateDao.kt @@ -11,8 +11,6 @@ interface ImageAggregateDao { /** * Observe a fully-hydrated image object. - * - * This is what your detail screen should use. */ @Transaction @Query(""" @@ -22,4 +20,29 @@ interface ImageAggregateDao { fun observeImageWithEverything( imageId: String ): Flow + + /** + * Observe all images. + */ + @Transaction + @Query(""" + SELECT * FROM images + ORDER BY capturedAt DESC + """) + fun observeAllImagesWithEverything(): Flow> + + /** + * Observe images filtered by tag value. + * + * Joins images -> image_tags -> tags + */ + @Transaction + @Query(""" + SELECT images.* FROM images + INNER JOIN image_tags ON images.imageId = image_tags.imageId + INNER JOIN tags ON tags.tagId = image_tags.tagId + WHERE tags.value = :tag + ORDER BY images.capturedAt DESC + """) + fun observeImagesWithTag(tag: String): Flow> } diff --git a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageTagDao.kt b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageTagDao.kt index 20bb08c..513e17a 100644 --- a/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageTagDao.kt +++ b/app/src/main/java/com/placeholder/sherpai2/data/local/dao/ImageTagDao.kt @@ -4,7 +4,9 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import com.placeholder.sherpai2.data.local.entity.ImageTagEntity +import com.placeholder.sherpai2.data.local.entity.TagEntity import kotlinx.coroutines.flow.Flow @Dao @@ -38,4 +40,14 @@ interface ImageTagDao { tagId: String, minConfidence: Float = 0.5f ): List + + @Transaction + @Query(""" + SELECT t.* + FROM tags t + INNER JOIN image_tags it ON t.tagId = it.tagId + WHERE it.imageId = :imageId AND it.visibility = 'PUBLIC' + """) + fun getTagsForImage(imageId: String): Flow> + } diff --git a/app/src/main/java/com/placeholder/sherpai2/di/RepositoryModule.kt b/app/src/main/java/com/placeholder/sherpai2/di/RepositoryModule.kt new file mode 100644 index 0000000..7c9f85a --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/di/RepositoryModule.kt @@ -0,0 +1,28 @@ +package com.placeholder.sherpai2.di + +import com.placeholder.sherpai2.data.repository.ImageRepositoryImpl +import com.placeholder.sherpai2.data.repository.TaggingRepositoryImpl +import com.placeholder.sherpai2.domain.repository.ImageRepository +import com.placeholder.sherpai2.domain.repository.TaggingRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindImageRepository( + impl: ImageRepositoryImpl + ): ImageRepository + + @Binds + @Singleton + abstract fun bindTaggingRepository( + impl: TaggingRepositoryImpl + ): TaggingRepository +} diff --git a/app/src/main/java/com/placeholder/sherpai2/domain/repository/ImageRepository.kt b/app/src/main/java/com/placeholder/sherpai2/domain/repository/ImageRepository.kt new file mode 100644 index 0000000..ce5f28f --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/domain/repository/ImageRepository.kt @@ -0,0 +1,31 @@ +package com.placeholder.sherpai2.domain.repository + +import com.placeholder.sherpai2.data.local.model.ImageWithEverything +import kotlinx.coroutines.flow.Flow + +/** + * Canonical access point for images. + * + * ViewModels must NEVER talk directly to DAOs. + */ +interface ImageRepository { + + /** + * Observe a fully-hydrated image graph. + * + * Used by detail screens. + */ + fun observeImage(imageId: String): Flow + + /** + * Ingest images discovered on device. + * + * This function: + * - deduplicates + * - assigns events automatically + */ + suspend fun ingestImages() + + fun getAllImages(): Flow> + fun findImagesByTag(tag: String): Flow> +} diff --git a/app/src/main/java/com/placeholder/sherpai2/domain/repository/ImageRepositoryImpl.kt b/app/src/main/java/com/placeholder/sherpai2/domain/repository/ImageRepositoryImpl.kt new file mode 100644 index 0000000..2d360f5 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/domain/repository/ImageRepositoryImpl.kt @@ -0,0 +1,64 @@ +package com.placeholder.sherpai2.data.repository + +import com.placeholder.sherpai2.data.local.dao.EventDao +import com.placeholder.sherpai2.data.local.dao.ImageAggregateDao +import com.placeholder.sherpai2.data.local.dao.ImageDao +import com.placeholder.sherpai2.data.local.dao.ImageEventDao +import com.placeholder.sherpai2.data.local.entity.ImageEventEntity +import com.placeholder.sherpai2.domain.repository.ImageRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ImageRepositoryImpl @Inject constructor( + private val imageDao: ImageDao, + private val eventDao: EventDao, + private val imageEventDao: ImageEventDao, + private val aggregateDao: ImageAggregateDao +) : ImageRepository { + + override fun observeImage(imageId: String): Flow { + return aggregateDao.observeImageWithEverything(imageId) + } + + /** + * Ingest images from device. + * + * NOTE: + * Actual MediaStore scanning is deliberately omitted here. + * This function assumes images already exist in ImageDao. + */ + override suspend fun ingestImages() { + // Step 1: fetch all images + val images = imageDao.getImagesInRange( + start = 0L, + end = Long.MAX_VALUE + ) + + // Step 2: auto-assign events by timestamp + images.forEach { image -> + val events = eventDao.findEventsForTimestamp(image.capturedAt) + + events.forEach { event -> + imageEventDao.upsert( + ImageEventEntity( + imageId = image.imageId, + eventId = event.eventId, + source = "AUTO", + override = false + ) + ) + } + } + } + + override fun getAllImages(): Flow> { + return aggregateDao.observeAllImagesWithEverything() + } + + override fun findImagesByTag(tag: String): Flow> { + // Assuming aggregateDao can filter images by tag + return aggregateDao.observeImagesWithTag(tag) + } +} diff --git a/app/src/main/java/com/placeholder/sherpai2/domain/repository/TaggingRepository.kt b/app/src/main/java/com/placeholder/sherpai2/domain/repository/TaggingRepository.kt new file mode 100644 index 0000000..fbc0d11 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/domain/repository/TaggingRepository.kt @@ -0,0 +1,30 @@ +package com.placeholder.sherpai2.domain.repository + +import com.placeholder.sherpai2.data.local.entity.TagEntity +import kotlinx.coroutines.flow.Flow + +/** + * Handles all tagging operations. + * + * This repository is the ONLY place where: + * - tags are attached + * - visibility rules are applied + */ +interface TaggingRepository { + + suspend fun addTagToImage( + imageId: String, + tagValue: String, + source: String, + confidence: Float + ) + + suspend fun hideTagForImage( + imageId: String, + tagValue: String + ) + + fun getTagsForImage(imageId: String): Flow> + + suspend fun removeTagFromImage(imageId: String, tagId: String) +} diff --git a/app/src/main/java/com/placeholder/sherpai2/domain/repository/TaggingRepositoryImpl.kt b/app/src/main/java/com/placeholder/sherpai2/domain/repository/TaggingRepositoryImpl.kt new file mode 100644 index 0000000..449efa8 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/domain/repository/TaggingRepositoryImpl.kt @@ -0,0 +1,97 @@ +package com.placeholder.sherpai2.data.repository + +import com.placeholder.sherpai2.data.local.dao.ImageTagDao +import com.placeholder.sherpai2.data.local.dao.TagDao +import com.placeholder.sherpai2.data.local.entity.ImageTagEntity +import com.placeholder.sherpai2.data.local.entity.TagEntity +import com.placeholder.sherpai2.domain.repository.TaggingRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * + * + * Critical design decisions here + * + * Tag normalization happens once + * + * Visibility rules live here + * + * ML and manual tagging share the same path + */ + +@Singleton +class TaggingRepositoryImpl @Inject constructor( + private val tagDao: TagDao, + private val imageTagDao: ImageTagDao +) : TaggingRepository { + + override suspend fun addTagToImage( + imageId: String, + tagValue: String, + source: String, + confidence: Float + ) { + // Step 1: normalize tag + val normalized = tagValue.trim().lowercase() + + // Step 2: ensure tag exists + val tag = tagDao.getByValue(normalized) + ?: TagEntity( + tagId = "tag_$normalized", + type = "GENERIC", + value = normalized, + createdAt = System.currentTimeMillis() + ).also { tagDao.insert(it) } + + // Step 3: attach tag to image + imageTagDao.upsert( + ImageTagEntity( + imageId = imageId, + tagId = tag.tagId, + source = source, + confidence = confidence, + visibility = "PUBLIC", + createdAt = System.currentTimeMillis() + ) + ) + } + + override suspend fun hideTagForImage( + imageId: String, + tagValue: String + ) { + val tag = tagDao.getByValue(tagValue) ?: return + + imageTagDao.upsert( + ImageTagEntity( + imageId = imageId, + tagId = tag.tagId, + source = "MANUAL", + confidence = 1.0f, + visibility = "HIDDEN", + createdAt = System.currentTimeMillis() + ) + ) + } + + override fun getTagsForImage(imageId: String): Flow> { + // Join imageTagDao -> tagDao to get all PUBLIC tags for this image + return imageTagDao.getTagsForImage(imageId) + } + + override suspend fun removeTagFromImage(imageId: String, tagId: String) { + // Mark the tag as hidden instead of deleting, keeping the visibility logic + imageTagDao.upsert( + ImageTagEntity( + imageId = imageId, + tagId = tagId, + source = "MANUAL", + confidence = 1.0f, + visibility = "HIDDEN", + createdAt = System.currentTimeMillis() + ) + ) + } +} diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/imagedetail/ImageDetailScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/imagedetail/ImageDetailScreen.kt new file mode 100644 index 0000000..b95bb24 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/ui/imagedetail/ImageDetailScreen.kt @@ -0,0 +1,86 @@ +package com.placeholder.sherpai2.ui.imagedetail + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import com.placeholder.sherpai2.ui.imagedetail.viewmodel.ImageDetailViewModel + + + + +/** + * ImageDetailScreen + * + * Purpose: + * - Add tags + * - Remove tags + * - Validate write propagation + */ +@Composable +fun ImageDetailScreen( + modifier: Modifier = Modifier, + imageUri: String, + onBack: () -> Unit +) { + val viewModel: ImageDetailViewModel = androidx.lifecycle.viewmodel.compose.viewModel() + + LaunchedEffect(imageUri) { + viewModel.loadImage(imageUri) + } + + val tags by viewModel.tags.collectAsStateWithLifecycle() + + var newTag by remember { mutableStateOf("") } + + Column( + modifier = modifier + .fillMaxSize() + .padding(12.dp) + ) { + + AsyncImage( + model = imageUri, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedTextField( + value = newTag, + onValueChange = { newTag = it }, + label = { Text("Add tag") }, + modifier = Modifier.fillMaxWidth() + ) + + Button( + onClick = { + viewModel.addTag(newTag) + newTag = "" + }, + modifier = Modifier.padding(top = 8.dp) + ) { + Text("Add Tag") + } + + Spacer(modifier = Modifier.height(16.dp)) + + tags.forEach { tag -> + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(tag.value) + TextButton(onClick = { viewModel.removeTag(tag) }) { + Text("Remove") + } + } + } + } +} diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/imagedetail/viewmodel/ImageDetailViewModel.kt b/app/src/main/java/com/placeholder/sherpai2/ui/imagedetail/viewmodel/ImageDetailViewModel.kt new file mode 100644 index 0000000..6c9b69e --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/ui/imagedetail/viewmodel/ImageDetailViewModel.kt @@ -0,0 +1,55 @@ +package com.placeholder.sherpai2.ui.imagedetail.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.placeholder.sherpai2.data.local.entity.TagEntity +import com.placeholder.sherpai2.domain.repository.TaggingRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * ImageDetailViewModel + * + * Owns: + * - Image context + * - Tag write operations + */ +@HiltViewModel +class ImageDetailViewModel @Inject constructor( + private val tagRepository: TaggingRepository +) : ViewModel() { + + private val imageUri = MutableStateFlow(null) + + val tags: StateFlow> = + imageUri + .filterNotNull() + .flatMapLatest { uri -> + tagRepository.getTagsForImage(uri) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) + + fun loadImage(uri: String) { + imageUri.value = uri + } + + fun addTag(value: String) { + val uri = imageUri.value ?: return + viewModelScope.launch { + tagRepository.addTagToImage(uri, value, source = "MANUAL", confidence = 1.0f) + } + } + + fun removeTag(tag: TagEntity) { + val uri = imageUri.value ?: return + viewModelScope.launch { + tagRepository.removeTagFromImage(uri, tag.value) + } + } +} diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/navigation/AppDestinations.kt b/app/src/main/java/com/placeholder/sherpai2/ui/navigation/AppDestinations.kt index ab4baf3..2560ffa 100644 --- a/app/src/main/java/com/placeholder/sherpai2/ui/navigation/AppDestinations.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/navigation/AppDestinations.kt @@ -1,4 +1,3 @@ -// In navigation/AppDestinations.kt package com.placeholder.sherpai2.ui.navigation import androidx.compose.material.icons.Icons @@ -6,27 +5,69 @@ import androidx.compose.material.icons.filled.* import androidx.compose.ui.graphics.vector.ImageVector /** - * Defines all navigation destinations (screens) for the application. + * AppDestinations + * + * Centralized definition of all top-level screens. + * This avoids stringly-typed navigation. */ -sealed class AppDestinations(val route: String, val icon: ImageVector, val label: String) { - // Core Functional Sections +sealed class AppDestinations( + val route: String, + val icon: ImageVector, + val label: String +) { + object Tour : AppDestinations( - route = "Tour", + 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") - object Train : AppDestinations("train", Icons.Default.TrackChanges, "Train") - object Tags : AppDestinations("tags", Icons.Default.LocalOffer, "Tags") - // Utility/Secondary Sections - object Upload : AppDestinations("upload", Icons.Default.CloudUpload, "Upload") - object Settings : AppDestinations("settings", Icons.Default.Settings, "Settings") + object Search : AppDestinations( + route = "search", + icon = Icons.Default.Search, + label = "Search" + ) + + object Models : AppDestinations( + route = "models", + icon = Icons.Default.Layers, + label = "Models" + ) + + object Inventory : AppDestinations( + route = "inventory", + icon = Icons.Default.Inventory2, + label = "Inventory" + ) + + object Train : AppDestinations( + route = "train", + icon = Icons.Default.TrackChanges, + label = "Train" + ) + + object Tags : AppDestinations( + route = "tags", + icon = Icons.Default.LocalOffer, + label = "Tags" + ) + + object Upload : AppDestinations( + route = "upload", + icon = Icons.Default.CloudUpload, + label = "Upload" + ) + + object Settings : AppDestinations( + route = "settings", + icon = Icons.Default.Settings, + label = "Settings" + ) } -// Lists used by the AppDrawerContent to render the menu sections easily +/** + * Drawer grouping + */ val mainDrawerItems = listOf( AppDestinations.Tour, AppDestinations.Search, @@ -39,4 +80,4 @@ val mainDrawerItems = listOf( val utilityDrawerItems = listOf( AppDestinations.Upload, AppDestinations.Settings -) \ No newline at end of file +) diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/navigation/MainScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/navigation/MainScreen.kt deleted file mode 100644 index 50ce1ce..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/ui/navigation/MainScreen.kt +++ /dev/null @@ -1,63 +0,0 @@ -// 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 kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -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: AppDestinations by remember { mutableStateOf(AppDestinations.Search) } - val galleryViewModel: GalleryViewModel = viewModel() - - // ModalNavigationDrawer provides the left sidebar UI/UX - ModalNavigationDrawer( - drawerState = drawerState, - drawerContent = { - // The content of the drawer (AppDrawerContent) - AppDrawerContent( - currentScreen = currentScreen, - onDestinationClicked = { destination -> - currentScreen = destination - scope.launch { drawerState.close() } // Close drawer after selection - } - ) - }, - ) { - // The main content area - Scaffold( - topBar = { - TopAppBar( - title = { Text(currentScreen.label) }, - // Button to open the drawer - navigationIcon = { - IconButton(onClick = { scope.launch { drawerState.open() } }) { - Icon(Icons.Filled.Menu, contentDescription = "Open Drawer") - } - } - ) - } - ) { paddingValues -> - // Displays the content for the currently selected screen - MainContentArea( - currentScreen = currentScreen, - galleryViewModel = galleryViewModel, - modifier = Modifier.padding(paddingValues) - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt deleted file mode 100644 index 53d2d25..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/AppDrawerContent.kt +++ /dev/null @@ -1,59 +0,0 @@ -// In presentation/AppDrawerContent.kt -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.ui.navigation.AppDestinations -import com.placeholder.sherpai2.ui.navigation.mainDrawerItems -import com.placeholder.sherpai2.ui.navigation.utilityDrawerItems - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AppDrawerContent( - currentScreen: AppDestinations, - onDestinationClicked: (AppDestinations) -> Unit -) { - // Defines the width and content of the sliding drawer panel - ModalDrawerSheet(modifier = Modifier.width(280.dp)) { - - // Header/Logo Area - Text( - "SherpAI Control Panel", - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier.padding(16.dp) - ) - Divider(Modifier.fillMaxWidth()) - - // 1. Main Navigation Items - Column(modifier = Modifier.padding(vertical = 8.dp)) { - mainDrawerItems.forEach { destination -> - NavigationDrawerItem( - label = { Text(destination.label) }, - icon = { Icon(destination.icon, contentDescription = destination.label) }, - selected = destination == currentScreen, - onClick = { onDestinationClicked(destination) }, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) - ) - } - } - - // Separator - Divider(Modifier.fillMaxWidth().padding(vertical = 8.dp)) - - // 2. Utility Items - Column(modifier = Modifier.padding(vertical = 8.dp)) { - utilityDrawerItems.forEach { destination -> - NavigationDrawerItem( - label = { Text(destination.label) }, - icon = { Icon(destination.icon, contentDescription = destination.label) }, - selected = destination == currentScreen, - onClick = { onDestinationClicked(destination) }, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) - ) - } - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 51541a3..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/MainContentArea.kt +++ /dev/null @@ -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) - ) -} - - - diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/PhotoListScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/presentation/PhotoListScreen.kt deleted file mode 100644 index 18815ed..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/ui/presentation/PhotoListScreen.kt +++ /dev/null @@ -1,34 +0,0 @@ -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 -) { - 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) - ) - } - } -} diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/search/SearchScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/search/SearchScreen.kt new file mode 100644 index 0000000..35d3956 --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/ui/search/SearchScreen.kt @@ -0,0 +1,70 @@ +package com.placeholder.sherpai2.ui.search + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.placeholder.sherpai2.ui.search.components.ImageGridItem +import com.placeholder.sherpai2.ui.search.SearchViewModel + +/** + * SearchScreen + * + * Purpose: + * - Validate tag-based queries + * - Preview matching images + * + * This is NOT final UX. + * It is a diagnostic surface. + */ +@Composable +fun SearchScreen( + modifier: Modifier = Modifier, + searchViewModel: SearchViewModel, + onImageClick: (String) -> Unit +) { + + var query by remember { mutableStateOf("") } + + /** + * Reactive result set. + * Updates whenever: + * - query changes + * - database changes + */ + val images by searchViewModel + .searchImagesByTag(query) + .collectAsStateWithLifecycle(initialValue = emptyList()) + + Column( + modifier = modifier + .fillMaxSize() + .padding(12.dp) + ) { + + OutlinedTextField( + value = query, + onValueChange = { query = it }, + label = { Text("Search by tag") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Spacer(modifier = Modifier.height(12.dp)) + + LazyVerticalGrid( + columns = GridCells.Adaptive(120.dp), + contentPadding = PaddingValues(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxSize() + ) { + items(images) { imageWithEverything -> + ImageGridItem(image = imageWithEverything.image) + } + } + } +} diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/search/SearchViewModel.kt b/app/src/main/java/com/placeholder/sherpai2/ui/search/SearchViewModel.kt new file mode 100644 index 0000000..8e7aa8e --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/ui/search/SearchViewModel.kt @@ -0,0 +1,24 @@ +package com.placeholder.sherpai2.ui.search + +import androidx.lifecycle.ViewModel +import com.placeholder.sherpai2.domain.repository.ImageRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * SearchViewModel + * + * Stateless except for query-driven flows. + */ +@HiltViewModel +class SearchViewModel @Inject constructor( + private val imageRepository: ImageRepository +) : ViewModel() { + + fun searchImagesByTag(tag: String) = + if (tag.isBlank()) { + imageRepository.getAllImages() + } else { + imageRepository.findImagesByTag(tag) + } +} diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/search/components/ImageGridItem.kt b/app/src/main/java/com/placeholder/sherpai2/ui/search/components/ImageGridItem.kt new file mode 100644 index 0000000..e877a3e --- /dev/null +++ b/app/src/main/java/com/placeholder/sherpai2/ui/search/components/ImageGridItem.kt @@ -0,0 +1,28 @@ +package com.placeholder.sherpai2.ui.search.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import coil.compose.rememberAsyncImagePainter +import com.placeholder.sherpai2.data.local.entity.ImageEntity + +/** + * ImageGridItem + * + * Minimal thumbnail preview. + * No click handling yet. + */ +@Composable +fun ImageGridItem( + image: ImageEntity +) { + Image( + painter = rememberAsyncImagePainter(image.imageUri), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) +} 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 deleted file mode 100644 index a101cff..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryScreen.kt +++ /dev/null @@ -1,73 +0,0 @@ -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 - ) -} \ 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 deleted file mode 100644 index 78f8dc2..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryUiState.kt +++ /dev/null @@ -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) : 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 deleted file mode 100644 index c521a17..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/GalleryViewModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -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.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") - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/components/AlbumBox.kt b/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/components/AlbumBox.kt deleted file mode 100644 index 1b376bc..0000000 --- a/app/src/main/java/com/placeholder/sherpai2/ui/tourscreen/components/AlbumBox.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.placeholder.sherpai2.ui.tourscreen.components - -class AlbumBox { -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6d15a2c..378452c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,8 @@ coil = "2.7.0" #backend2 #Room room = "2.6.1" +composeMaterial3 = "1.5.6" +material3 = "1.4.0" [libraries] @@ -52,6 +54,8 @@ coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coi room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } [plugins]