From 726d1e004546ef595659f3511d461e3bd8668c33 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 4 Dec 2025 13:38:33 -0800 Subject: [PATCH 001/307] chore(demo): Refactor navigation demo components to private visibility This commit moves the `PhotoDoAppNav3.kt` file to a new `demo.navigation` package, clarifying its role as a demonstration component. To encapsulate the demo logic and prevent its components from being used outside their intended context, all composables and sealed classes within the file have been made `private`. This enforces better modularity and makes the code easier to maintain. **Key Changes:** * **File Location:** Moved `PhotoDoAppNav3.kt` from `ui/navigation/main` to `ui/demo/navigation`. * **Visibility Modifiers:** * The `AppScreen` sealed class and its corresponding `AppScreenSaver` are now `private`. * All composable functions (`SimpleAdaptiveBottomBar`, `AppContent`, `AppBottomBar`, `AppNavigationRail`, `ListScreen`, `DetailScreen`, and `ScreenContent`) have been changed to `private`. --- .../navigation}/PhotoDoAppNav3.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) rename applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/{navigation/main => demo/navigation}/PhotoDoAppNav3.kt (94%) diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/PhotoDoAppNav3.kt b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/demo/navigation/PhotoDoAppNav3.kt similarity index 94% rename from applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/PhotoDoAppNav3.kt rename to applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/demo/navigation/PhotoDoAppNav3.kt index a27a2d593..afdec0bf6 100644 --- a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/PhotoDoAppNav3.kt +++ b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/demo/navigation/PhotoDoAppNav3.kt @@ -1,4 +1,4 @@ -package com.ylabz.basepro.applications.photodo.ui.navigation.main // Change this to your package name +package com.ylabz.basepro.applications.photodo.ui.demo.navigation // Change this to your package name import android.annotation.SuppressLint import android.app.Activity @@ -51,7 +51,7 @@ import kotlinx.serialization.Transient // --- STEP 1: DEFINE DESTINATIONS (UPDATED) --- @Serializable -sealed class AppScreen(val title: String) : NavKey { +private sealed class AppScreen(val title: String) : NavKey { @Transient abstract val icon: ImageVector @@ -86,7 +86,7 @@ sealed class AppScreen(val title: String) : NavKey { private val topLevelScreens = listOf(AppScreen.Home, AppScreen.Search, AppScreen.Profile) // Updated Saver to handle all AppScreen types. -val AppScreenSaver = Saver( +private val AppScreenSaver = Saver( save = { it.title }, restore = { title -> topLevelScreens.find { it.title == title } ?: AppScreen.Home } ) @@ -96,7 +96,7 @@ val AppScreenSaver = Saver( @OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalMaterial3AdaptiveApi::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable -fun SimpleAdaptiveBottomBar() { +private fun SimpleAdaptiveBottomBar() { val activity = LocalActivity.current as Activity val windowSizeClass = calculateWindowSizeClass(activity) val isExpandedScreen = windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact @@ -151,7 +151,7 @@ fun SimpleAdaptiveBottomBar() { @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -fun AppContent( +private fun AppContent( backStack: NavBackStack, sceneStrategy: ListDetailSceneStrategy ) { @@ -184,7 +184,7 @@ fun AppContent( } @Composable -fun AppBottomBar(currentTab: AppScreen, onNavigate: (AppScreen) -> Unit) { +private fun AppBottomBar(currentTab: AppScreen, onNavigate: (AppScreen) -> Unit) { NavigationBar { topLevelScreens.forEach { screen -> val isSelected = currentTab.title == screen.title @@ -199,7 +199,7 @@ fun AppBottomBar(currentTab: AppScreen, onNavigate: (AppScreen) -> Unit) { } @Composable -fun AppNavigationRail(currentTab: AppScreen, onNavigate: (AppScreen) -> Unit) { +private fun AppNavigationRail(currentTab: AppScreen, onNavigate: (AppScreen) -> Unit) { NavigationRail { topLevelScreens.forEach { screen -> val isSelected = currentTab.title == screen.title @@ -216,7 +216,7 @@ fun AppNavigationRail(currentTab: AppScreen, onNavigate: (AppScreen) -> Unit) { // --- STEP 4: ADD NEW SCREEN COMPOSABLES --- @Composable -fun ListScreen(onItemClick: (Int) -> Unit) { +private fun ListScreen(onItemClick: (Int) -> Unit) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, @@ -231,14 +231,14 @@ fun ListScreen(onItemClick: (Int) -> Unit) { } @Composable -fun DetailScreen(id: Int) { +private fun DetailScreen(id: Int) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(text = "Detail for item #$id", style = MaterialTheme.typography.headlineLarge) } } @Composable -fun ScreenContent(name: String) { +private fun ScreenContent(name: String) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(text = name, style = MaterialTheme.typography.headlineLarge) } From f42b95c5d965cb0ac5ec0e040a773b51ea45e64c Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 4 Dec 2025 14:25:30 -0800 Subject: [PATCH 002/307] refactor(nav): Centralize back stack logic for tablet layouts This commit refactors the navigation logic within `HomeEntry.kt` to better handle state changes on tablet (`isExpandedScreen`) and phone layouts, particularly for category and task list selections. The responsibility for manipulating the navigation `backStack` is now centralized. Instead of being handled directly within UI event callbacks (`onCategoryClick`, `onTaskListClick`), this logic is now managed by the primary navigation event handlers. This ensures a cleaner separation of concerns and more predictable navigation behavior. **Key Changes:** * **`onTaskListSelected` (Tablet Logic):** * When a task list is selected on a tablet, the navigation `backStack` is now cleared of any "drill-down" screens (any screen after the first one). This ensures the detail pane correctly displays the newly selected list without retaining a deeper navigation history from a previous selection. * **`onCategoryClick` (Phone Logic Removed):** * The code that manually added the `TaskListKey` to the `backStack` for phone layouts has been removed from the `onCategoryClick` callback. * This callback now only dispatches the `HomeEvent.OnCategorySelected` event. The actual navigation stack manipulation is handled elsewhere, likely triggered by the state change resulting from this event. --- .../photodo/ui/navigation/main/entries/HomeEntry.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/entries/HomeEntry.kt b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/entries/HomeEntry.kt index fbc79819d..3a13c4b5e 100644 --- a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/entries/HomeEntry.kt +++ b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/entries/HomeEntry.kt @@ -107,6 +107,14 @@ fun HomeEntry( // 3. SWITCH TABS: Clear the stack and start fresh with the Task List backStack.clear() backStack.add(taskTabKey) + } else { + // Tablet logic: Clear drill-down to show new detail + if (backStack.size > 1) { + backStack.subList(1, backStack.size).clear() + } + // Note: On tablets, we don't necessarily "navigate" + // because the detail pane updates via state, + // but you might want to ensure the TaskListKey is present. } } } @@ -247,12 +255,13 @@ fun HomeEntry( onCategoryClick = { categoryId -> // ... (Keep existing logic) ... + // ONLY trigger the event. Do NOT manipulate backStack here. homeViewModel.onEvent(HomeEvent.OnCategorySelected(categoryId)) - if (!isExpandedScreen) { + /*if (!isExpandedScreen) { val newTaskListKey = PhotoDoNavKeys.TaskListKey(categoryId) if (backStack.size > 1) { backStack.subList(1, backStack.size).clear() } backStack.add(newTaskListKey) - } + }*/ }, // --- IMPLEMENT NEW CALLBACK --- From a5d70f54ac14908c22da852555ab63d200ae7d53 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 4 Dec 2025 14:52:29 -0800 Subject: [PATCH 003/307] style(home): Enhance `CategoryCard` selection with animations and a border This commit improves the visual feedback when a `CategoryCard` is selected by introducing several UI enhancements. These changes make the selected state more prominent and visually distinct from unselected cards. **Key Changes in `CategoryCard.kt`:** * **Border:** A 2dp border using the primary color is now applied to the card when it is selected. * **Background Color:** The container color now animates to `surfaceContainerHighest` on selection, making it brighter and more prominent, and animates back to `surface` when unselected. * **Scale Animation:** A subtle scaling animation has been added, making the card "pop" by scaling it up to 1.02f when selected. --- .../home/ui/components/CategoryCard.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt index ed09e8f3a..0ee6c6ad9 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt @@ -2,7 +2,9 @@ package com.ylabz.basepro.applications.photodo.features.home.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -61,7 +63,6 @@ fun CategoryCard( ) { // Extract values for ease of use val (category, isSelected, taskLists) = uiState - var isExpanded by rememberSaveable { mutableStateOf(false) } // Auto-collapse if selection is lost @@ -75,12 +76,32 @@ fun CategoryCard( val hasHighPriority = taskLists.any { it.priority > 0 } // -------------------------- + // 1. ANIMATE COLOR: Darker when unselected, brighter/tinted when selected val containerColor by animateColorAsState( - targetValue = if (isSelected) MaterialTheme.colorScheme.surfaceContainerHigh else MaterialTheme.colorScheme.surfaceContainer, + targetValue = if (isSelected) + MaterialTheme.colorScheme.surfaceContainerHighest // Lighter/Prominent + else + MaterialTheme.colorScheme.surface, // Flatter/Receded animationSpec = tween(durationMillis = 300), label = "CardBackground" ) + // 2. ANIMATE SCALE: Slight zoom when selected + val scale by animateFloatAsState( + targetValue = if (isSelected) 1.02f else 1f, + animationSpec = tween(durationMillis = 300), + label = "Scale" + ) + + // 3. BORDER: The strongest visual cue + val borderStroke = if (isSelected) { + BorderStroke(2.dp, MaterialTheme.colorScheme.primary) + } else { + BorderStroke(0.dp, Color.Transparent) + } + + // --- POP VISUALS END --- + val coverGradient = remember(category.name) { val hash = category.name.hashCode() val hue = (hash % 360).toFloat() From 09b8a9d2e914d2e2edd3fa6912387f785049d199 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 4 Dec 2025 15:38:41 -0800 Subject: [PATCH 004/307] refactor(home): Enhance CategoryCard with expressive selection animations This commit refactors the `CategoryCard` to introduce more expressive and "lively" animations when a category is selected, improving the user experience with tactile and visual feedback. The animations now use spring physics for a more natural, bouncy feel instead of simple tweens. The visual changes upon selection are more pronounced to clearly highlight the active card. **Key Changes in `CategoryCard.kt`:** * **Expressive Motion:** * Introduced a "bouncy" spring animation (`Spring.DampingRatioLowBouncy`) to scale the card up to `1.05f` on selection, making it feel more responsive. * Replaced `tween` animations with `spring` physics for color and shape changes for a more fluid feel. * **Shape & Style Morphing:** * The card's corner radius now animates from `12dp` to `32dp` on selection, creating a distinct shape change. * The title typography becomes larger and bolder (`headlineSmall`, `ExtraBold`) and its color changes to `primary` when selected. * The border is now applied using the animated card shape. * **Tactile & Visual Feedback:** * Added haptic feedback (`HapticFeedbackType.LongPress`) on card click to provide a physical response. * The clickable area now uses a `null` indication to emphasize the custom spring animation over the default ripple effect. * **UI Structure:** * The action buttons (Expand, Edit, Delete) are now wrapped in an `AnimatedVisibility` block, so they only appear when the card is selected. --- .../home/ui/components/CategoryCard.kt | 163 ++++++++++++------ 1 file changed, 112 insertions(+), 51 deletions(-) diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt index 0ee6c6ad9..6380ae966 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt @@ -2,11 +2,15 @@ package com.ylabz.basepro.applications.photodo.features.home.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -46,9 +50,12 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -64,6 +71,7 @@ fun CategoryCard( // Extract values for ease of use val (category, isSelected, taskLists) = uiState var isExpanded by rememberSaveable { mutableStateOf(false) } + val haptics = LocalHapticFeedback.current // 1. Haptics access // Auto-collapse if selection is lost LaunchedEffect(isSelected) { @@ -76,30 +84,47 @@ fun CategoryCard( val hasHighPriority = taskLists.any { it.priority > 0 } // -------------------------- - // 1. ANIMATE COLOR: Darker when unselected, brighter/tinted when selected - val containerColor by animateColorAsState( - targetValue = if (isSelected) - MaterialTheme.colorScheme.surfaceContainerHighest // Lighter/Prominent - else - MaterialTheme.colorScheme.surface, // Flatter/Receded - animationSpec = tween(durationMillis = 300), - label = "CardBackground" - ) + // --- EXPRESSIVE MOTION & SHAPE --- - // 2. ANIMATE SCALE: Slight zoom when selected + // 2. SPRING ANIMATION: "Bouncy" scale (StiffnessLow + DampingRatioLowBouncy) + // This creates that "alive" feeling when the card is tapped. val scale by animateFloatAsState( - targetValue = if (isSelected) 1.02f else 1f, - animationSpec = tween(durationMillis = 300), - label = "Scale" + targetValue = if (isSelected) 1.05f else 1f, // Increased scale for more drama + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessLow + ), + label = "ExpressiveScale" + ) + + // 3. SHAPE MORPH: Animate corner radius + // Unselected = 16dp (Medium), Selected = 32dp (Extra Large) + // This physically changes the silhouette of the focused item. + val cornerRadius by animateIntAsState( + targetValue = if (isSelected) 32 else 12, + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "ShapeMorph" + ) + val cardShape = RoundedCornerShape(cornerRadius.dp) + + // 4. COLOR SPRING: Ensure color changes are also fluid + val containerColor by animateColorAsState( + targetValue = if (isSelected) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.surface, + animationSpec = spring(stiffness = Spring.StiffnessLow), + label = "CardBackground" ) - // 3. BORDER: The strongest visual cue val borderStroke = if (isSelected) { BorderStroke(2.dp, MaterialTheme.colorScheme.primary) } else { BorderStroke(0.dp, Color.Transparent) } + // --------------------------------- + // --- POP VISUALS END --- val coverGradient = remember(category.name) { @@ -113,7 +138,16 @@ fun CategoryCard( ElevatedCard( modifier = modifier .fillMaxWidth() - .clickable { onEvent(HomeEvent.OnCategorySelected(category.categoryId)) }, + .scale(scale) + .border(borderStroke, shape = cardShape) // Use animated shape + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null // Remove default ripple if you want pure spring feel, or keep it + ) { + // Effects + haptics.performHapticFeedback(HapticFeedbackType.LongPress) // Physical feedback + onEvent(HomeEvent.OnCategorySelected(category.categoryId)) + }, shape = MaterialTheme.shapes.extraLarge, colors = CardDefaults.elevatedCardColors(containerColor = containerColor), elevation = CardDefaults.elevatedCardElevation( @@ -122,7 +156,9 @@ fun CategoryCard( ) { Column { Row( - modifier = Modifier.fillMaxWidth().height(130.dp) + modifier = Modifier + .fillMaxWidth() + .height(130.dp) ) { // 1. COVER IMAGE Box( @@ -151,14 +187,20 @@ fun CategoryCard( // 2. CONTENT Column( - modifier = Modifier.weight(1f).padding(12.dp) + modifier = Modifier + .weight(1f) + .padding(12.dp) ) { // Title Row Row(verticalAlignment = Alignment.CenterVertically) { Text( text = category.name, - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.onSurface, + // 5. TYPOGRAPHY: Bigger and bolder title when selected + style = if (isSelected) + MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.ExtraBold) + else + MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold), + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) @@ -215,42 +257,60 @@ fun CategoryCard( // ... (Actions Row and Accordion remain the same as previous code) ... // I'll include the Actions Row here for completeness of the 'Column' block - - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically - ) { - IconButton( - onClick = { - if (!isSelected) onEvent(HomeEvent.OnCategorySelected(category.categoryId)) - isExpanded = !isExpanded +// ACTIONS (Visible only on selection) + AnimatedVisibility(visible = isSelected) { + Column { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { + if (!isSelected) onEvent(HomeEvent.OnCategorySelected(category.categoryId)) + isExpanded = !isExpanded + } + ) { + Icon( + imageVector = if (isExpanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, + contentDescription = "Expand", + tint = MaterialTheme.colorScheme.primary + ) + } + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = { onEvent(HomeEvent.OnEditCategoryClicked(category)) }, + modifier = Modifier.size(32.dp) + ) { + Icon( + Icons.Default.Edit, + "Edit", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + IconButton( + onClick = { onEvent(HomeEvent.OnDeleteCategoryClicked(category)) }, + modifier = Modifier.size(32.dp) + ) { + Icon( + Icons.Default.Delete, + "Delete", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp) + ) + } } - ) { - Icon( - imageVector = if (isExpanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, - contentDescription = "Expand", - tint = MaterialTheme.colorScheme.primary - ) - } - Spacer(modifier = Modifier.weight(1f)) - IconButton( - onClick = { onEvent(HomeEvent.OnEditCategoryClicked(category)) }, - modifier = Modifier.size(32.dp) - ) { - Icon(Icons.Default.Edit, "Edit", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp)) - } - Spacer(modifier = Modifier.width(8.dp)) - IconButton( - onClick = { onEvent(HomeEvent.OnDeleteCategoryClicked(category)) }, - modifier = Modifier.size(32.dp) - ) { - Icon(Icons.Default.Delete, "Delete", tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(20.dp)) } } // ... (Accordion Section remains the same) ... + // ACTIONS (Visible only on selection) AnimatedVisibility(visible = isSelected && isExpanded) { Column( modifier = Modifier @@ -270,7 +330,8 @@ fun CategoryCard( Row( modifier = Modifier .fillMaxWidth() - .clickable { onEvent(HomeEvent.OnTaskListSelected(taskList.listId)) } .padding(vertical = 8.dp), + .clickable { onEvent(HomeEvent.OnTaskListSelected(taskList.listId)) } + .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( From 056a9f89889a6f7608ac35dfa84ad9f8bcf09878 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 4 Dec 2025 15:59:26 -0800 Subject: [PATCH 005/307] refactor(home): Prevent automatic navigation on category selection This commit refactors the category selection logic on the Home screen to improve the user experience on non-expanded (phone) layouts. Previously, selecting a category card would immediately navigate the user away from the Home screen to the corresponding task list. This behavior was disruptive. With this change, when a user selects a category on a phone, the card becomes highlighted, but the app remains on the Home screen. The navigation to the task list is no longer automatic, allowing the user to explicitly choose to navigate via other UI elements (like a bottom navigation bar). On expanded (tablet) layouts, the existing behavior is preserved: selecting a category updates the detail pane to show the relevant task list. **Key Change:** * **`HomeEntry.kt`**: * Removed the code block that would automatically clear the back stack and add a new `TaskListKey` when `!isExpandedScreen`. This stops the forced navigation on phones. * Added comments to clarify the distinct logic for tablet and phone layouts. --- .../ui/navigation/main/entries/HomeEntry.kt | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/entries/HomeEntry.kt b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/entries/HomeEntry.kt index 3a13c4b5e..0fb77ba4b 100644 --- a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/entries/HomeEntry.kt +++ b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/entries/HomeEntry.kt @@ -87,35 +87,26 @@ fun HomeEntry( // 1. Notify the MainScreen's ViewModel (which stores lastSelectedCategoryId) onCategorySelected(categoryId) - val newTaskListKey = PhotoDoNavKeys.HomeTaskListKey(categoryId) + // 2. TABLET LOGIC (Split Screen) + // On a big screen, we DO want to update the right-hand pane immediately. + if (isExpandedScreen) { + Log.d(TAG, "Tablet: Updating detail pane for Category $categoryId") - // 2. CRITICAL STEP: Clear all existing drill-down items (Columns 2 and 3). - // This prevents bloat when switching categories (Family -> Shopping). - // Navigate (Always add to stack on explicit click) - if (backStack.size > 1) { - Log.d(TAG, "Category Selected. Resetting stack from size ${backStack.size}.") - // Remove everything after the first item (HomeFeedKey). - backStack.subList(1, backStack.size).clear() - } - // backStack.add(newTaskListKey) clicking on a category should never show an Item - // 3. Add the new TaskListKey. This adds Column 2 content. - // Log.d(TAG, "Adding new TaskListKey($categoryId) to backStack.") - if (!isExpandedScreen) { - // 2. Create the destination Key (The root of the Tasks tab) - val taskTabKey = PhotoDoNavKeys.TaskListKey(categoryId) - - // 3. SWITCH TABS: Clear the stack and start fresh with the Task List - backStack.clear() - backStack.add(taskTabKey) - } else { - // Tablet logic: Clear drill-down to show new detail + val newTaskListKey = PhotoDoNavKeys.HomeTaskListKey(categoryId) + + // Clear previous drill-downs to avoid stacking multiple detail panes if (backStack.size > 1) { backStack.subList(1, backStack.size).clear() } - // Note: On tablets, we don't necessarily "navigate" - // because the detail pane updates via state, - // but you might want to ensure the TaskListKey is present. + + // Show the new list on the right + backStack.add(newTaskListKey) } + + // 3. PHONE LOGIC (Closed) + // We do NOTHING here. + // We removed the `if (!isExpandedScreen) { backStack.add(...) }` block. + // Result: User stays on Home, Card highlights, no forced jump. } } // ---------------------- From 27f301c005259e86bbf016ca69d952c7fe0d853b Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 4 Dec 2025 17:19:37 -0800 Subject: [PATCH 006/307] refactor(detail): Redesign DetailCard with expressive UI and animations This commit completely revamps the `DetailCard` composable, replacing its previous simple layout with a modern, expressive, and interactive design. The new card incorporates animations, haptic feedback, and a clearer visual hierarchy to improve user experience. **Key Changes:** * **Expressive Animations & Feedback**: * Introduces physics-based animations (spring) for press (`scale`), corner rounding (`cornerRadius`), and color changes (`animateColorAsState`). * Integrates haptic feedback (`HapticFeedbackType.LongPress`, `ContextClick`) on interactions to provide a tactile response. * The default ripple indication on click is removed to emphasize the new physics-based animations. * **Visual Redesign**: * The card layout is redesigned with more generous padding and spacing. * Typography is enhanced with bolder weights and tighter letter spacing for headlines. * A progress section with a thick, rounded `LinearProgressIndicator` has been added to visualize task completion based on photo count. * **New Components**: * A private `ExpressiveActionButton` composable has been created to provide a consistent style for high-emphasis actions like "Add Photo" and "Gallery". This replaces the previous "Add Photo" card and simple buttons. * **Refactored Logic**: * The previous checklist functionality, including checkboxes and an "add item" text field, has been removed. * The card now focuses on displaying the task list name, description, photo progress, and primary actions. * The photo display logic (previously a `LazyRow`) has been replaced by the new action buttons. --- .../ui/detail/components/DetailCard.kt | 371 +++++++++++------- 1 file changed, 231 insertions(+), 140 deletions(-) diff --git a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt index b6361378a..493f217af 100644 --- a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt +++ b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt @@ -1,190 +1,281 @@ package com.ylabz.basepro.applications.photodo.features.photodolist.ui.detail.components +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.Checkbox +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.PhotoLibrary +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.ylabz.basepro.applications.photodo.db.entity.TaskListWithPhotos import com.ylabz.basepro.applications.photodo.features.photodolist.ui.detail.PhotoDoDetailEvent @Composable fun DetailCard( modifier: Modifier = Modifier, - taskListWithPhotos: TaskListWithPhotos, // UI State for this component - onEvent: (PhotoDoDetailEvent) -> Unit, // Single Event Handler + taskListWithPhotos: TaskListWithPhotos, + onEvent: (PhotoDoDetailEvent) -> Unit ) { - // --- Local State for the "New Item" text field --- - var newItemText by remember { mutableStateOf("") } + val (taskList, photos) = taskListWithPhotos - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // --- 1. HEADER INFO --- - ElevatedCard(modifier = Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp)) { - Text( - text = taskListWithPhotos.taskList.name, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - if (!taskListWithPhotos.taskList.notes.isNullOrBlank()) { - Spacer(Modifier.height(8.dp)) - Text( - text = taskListWithPhotos.taskList.notes!!, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } + // --- 1. EXPRESSIVE STATE MANAGEMENT --- + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val haptics = LocalHapticFeedback.current - // --- 2. CHECKLIST --- - Text( - text = "Checklist", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary - ) + // Spring Physics: Bouncy scale effect on press + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.97f else 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "CardScale" + ) + + // Shape Morph: Corners get rounder when pressed + val cornerRadius by animateIntAsState( + targetValue = if (isPressed) 32 else 24, // Morph from Large to Extra Large + animationSpec = spring(stiffness = Spring.StiffnessLow), + label = "CornerMorph" + ) - ElevatedCard(modifier = Modifier.fillMaxWidth()) { - Column { - if (taskListWithPhotos.items.isEmpty()) { + // Color: Subtle tint shift + val containerColor by animateColorAsState( + targetValue = if (isPressed) + MaterialTheme.colorScheme.surfaceContainerHigh + else + MaterialTheme.colorScheme.surfaceContainer, + label = "ColorShift" + ) + + // -------------------------------------- + + ElevatedCard( + modifier = modifier + .fillMaxWidth() + .scale(scale) // Apply physics scale + .clickable( + interactionSource = interactionSource, + indication = null // Disable default ripple for cleaner physics feel + ) { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + // Optional: Navigate to full edit screen or expand + }, + shape = RoundedCornerShape(cornerRadius.dp), + colors = CardDefaults.elevatedCardColors(containerColor = containerColor), + elevation = CardDefaults.elevatedCardElevation( + defaultElevation = if (isPressed) 2.dp else 6.dp // Depress effect + ) + ) { + Column( + modifier = Modifier.padding(24.dp) // Generous "Expressive" spacing + ) { + // --- HEADER ROW --- + Row( + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.weight(1f)) { Text( - text = "No items added.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(16.dp) + text = taskList.name, + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.ExtraBold, // Bolder is better + letterSpacing = (-0.5).sp // Tighter tracking for headlines + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis ) - } else { - taskListWithPhotos.items.forEach { item -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = item.isChecked, - onCheckedChange = { isChecked -> - // Trigger Event - onEvent(PhotoDoDetailEvent.OnItemCheckedChange(item, isChecked)) - } - ) - Text( - text = item.text, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f) - ) - - // --- ADDED DELETE BUTTON --- - IconButton( - onClick = { onEvent(PhotoDoDetailEvent.OnDeleteItem(item)) } - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete Item", - tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f) - ) - } - // --------------------------- - - - } - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) - } + Spacer(modifier = Modifier.height(4.dp)) + // if (!taskList.name.isNullOrBlank()) { + Text( + text = "Description",//taskList.discription ?: "", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + //} } - // --- ADD ITEM ROW --- - Row( + // Action Button (Edit) + IconButton( + onClick = { + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) + // onEvent(PhotoDoDetailEvent.OnEditList(taskList)) + }, modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically + .background( + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), + CircleShape + ) ) { - OutlinedTextField( - value = newItemText, - onValueChange = { newItemText = it }, - label = { Text("New Item") }, - modifier = Modifier.weight(1f), - singleLine = true + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit List", + tint = MaterialTheme.colorScheme.onPrimaryContainer ) - - Spacer(Modifier.width(8.dp)) - - IconButton( - onClick = { - if (newItemText.isNotBlank()) { - onEvent(PhotoDoDetailEvent.OnAddItemClicked(newItemText)) - newItemText = "" // Clear input after adding - } - } - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add Item", - tint = MaterialTheme.colorScheme.primary - ) - } } } - } - // --- 3. PHOTOS --- - Text( - text = "Photos", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary - ) + Spacer(modifier = Modifier.height(24.dp)) - LazyRow( - horizontalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(horizontal = 4.dp) - ) { - item { - AddPhotoCard(onClick = { onEvent(PhotoDoDetailEvent.OnCameraClick) }) // Trigger Event + // --- PROGRESS SECTION --- + // Calculate progress (e.g., if you have a 'total' target, or just count photos) + // Assuming simplified logic: 1 photo = 10% progress for visual demo, + // or modify 'TaskListEntity' to have a 'targetPhotoCount'. + val photoCount = photos.size + val targetCount = 10 // Arbitrary target for visualization + val progress = (photoCount / targetCount.toFloat()).coerceIn(0f, 1f) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "$photoCount Photos", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "Goal: $targetCount", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.outline + ) } - items(taskListWithPhotos.photos, key = { it.photoId }) { photo -> - PhotoItem( - photo = photo, - onDeleteClick = { - // onEvent(PhotoDoDetailEvent.OnDeletePhotoClicked(photo.photoId)) // Trigger Event - } + + Spacer(modifier = Modifier.height(8.dp)) + + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier + .fillMaxWidth() + .height(12.dp) // Thicker, expressive bar + .clip(RoundedCornerShape(50)), // Pill shape + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + strokeCap = StrokeCap.Round, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // --- EXPRESSIVE ACTIONS ROW --- + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // "Add Photo" Chip - High Emphasis + ExpressiveActionButton( + text = "Add Photo", + icon = Icons.Default.Add, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.weight(1f), + onClick = {}// onEvent(PhotoDoDetailEvent.OnPhotoSaved(taskList.listId)) } + ) + + // "View Gallery" Chip - Medium Emphasis + ExpressiveActionButton( + text = "Gallery", + icon = Icons.Default.PhotoLibrary, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.weight(1f), + onClick = { /* Navigate to gallery view */ } ) } } + } +} - Spacer(Modifier.height(80.dp)) +/** + * A custom helper button that matches the Expressive style: + * - Tall height (56dp) + * - Fully rounded corners + */ +@Composable +private fun ExpressiveActionButton( + text: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + containerColor: Color, + contentColor: Color, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + val haptics = LocalHapticFeedback.current + + Box( + modifier = modifier + .height(56.dp) + .clip(RoundedCornerShape(16.dp)) // Medium rounded + .background(containerColor) + .clickable { + haptics.performHapticFeedback(HapticFeedbackType.ContextClick)//.LightImpact) + onClick() + }, + contentAlignment = Alignment.Center + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = icon, + contentDescription = null, + tint = contentColor, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold), + color = contentColor + ) + } } -} \ No newline at end of file +} + +/* Extension for convenient SP unit if not available +private val Int.sp: androidx.compose.ui.unit.TextUnit + get() = androidx.compose.ui.unit.sp(this) +private val Double.sp: androidx.compose.ui.unit.TextUnit + get() = androidx.compose.ui.unit.sp(this)*/ \ No newline at end of file From 15809fdf466d1388f1728adb456d19fba2c24f53 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 4 Dec 2025 18:23:45 -0800 Subject: [PATCH 007/307] refactor(detail): Redesign detail screen with checklist and photo management This commit completely redesigns the `PhotoDoDetail` screen, moving from a static summary card to an interactive and modular layout. It introduces a fully functional checklist for managing task items and a dedicated section for adding and deleting photos. The `DetailCard` composable has been refactored into a collection of smaller, more focused components to build the new UI. **Key Changes:** * **`PhotoDoDetailEvents.kt`**: * Added a new `OnEditList` event to handle editing the properties of the current task list. * **`DetailCard.kt`**: * The entire composable has been rewritten to orchestrate the new detail screen layout. * **Hero Header**: A new `HeroHeaderCard` displays the list's title and notes, with an "Edit" button that triggers the `OnEditList` event. * **Checklist Management**: * The screen now displays a list of `TaskItemEntity` objects. * Users can add new items via a text field and an "Add" button, triggering an `OnAddItemClicked` event. * Existing items can be checked/unchecked (`OnItemCheckedChange`) or deleted (`OnDeleteItem`). * **Photo Management**: * A new `LazyRow` displays photo thumbnails. * An `ExpressiveAddPhotoCard` provides a clear call-to-action for taking a new photo. * Each photo has a delete icon that triggers the `OnDeletePhoto` event. * **Componentization**: The old, monolithic `DetailCard` has been broken down into specialized composables like `HeroHeaderCard`, `ExpressiveChecklistItem`, `PhotoItem`, and `SectionTitle` for better organization and reusability. --- .../ui/detail/PhotoDoDetailEvents.kt | 3 + .../ui/detail/components/DetailCard.kt | 488 +++++++++++------- 2 files changed, 316 insertions(+), 175 deletions(-) diff --git a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/PhotoDoDetailEvents.kt b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/PhotoDoDetailEvents.kt index 6481dc6df..1b9a48c7d 100644 --- a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/PhotoDoDetailEvents.kt +++ b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/PhotoDoDetailEvents.kt @@ -2,12 +2,15 @@ package com.ylabz.basepro.applications.photodo.features.photodolist.ui.detail import android.net.Uri import com.ylabz.basepro.applications.photodo.db.entity.TaskItemEntity +import com.ylabz.basepro.applications.photodo.db.entity.TaskListEntity /** * Defines the events that can be triggered from the PhotoDoDetail screen. */ sealed interface PhotoDoDetailEvent { + data class OnEditList(val taskList: TaskListEntity) : PhotoDoDetailEvent + /** * Event triggered when the user saves a new photo from the camera. * @param uri The URI of the saved photo. diff --git a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt index 493f217af..99c0060d2 100644 --- a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt +++ b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt @@ -1,10 +1,7 @@ package com.ylabz.basepro.applications.photodo.features.photodolist.ui.detail.components import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.animateIntAsState -import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -12,41 +9,61 @@ import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.CameraAlt +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.PhotoLibrary +import androidx.compose.material.icons.filled.Image import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable 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.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.ylabz.basepro.applications.photodo.db.entity.PhotoEntity import com.ylabz.basepro.applications.photodo.db.entity.TaskListWithPhotos import com.ylabz.basepro.applications.photodo.features.photodolist.ui.detail.PhotoDoDetailEvent @@ -56,62 +73,197 @@ fun DetailCard( taskListWithPhotos: TaskListWithPhotos, onEvent: (PhotoDoDetailEvent) -> Unit ) { - val (taskList, photos) = taskListWithPhotos + val (taskList, photos, items) = taskListWithPhotos + var newItemText by remember { mutableStateOf("") } + val scrollState = rememberScrollState() + val haptics = LocalHapticFeedback.current + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(horizontal = 16.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // --- 1. HERO HEADER --- + HeroHeaderCard( + title = taskList.name, + notes = taskList.notes, + onEditClick = { onEvent(PhotoDoDetailEvent.OnEditList(taskList)) } + ) + + // --- 2. CHECKLIST SECTION --- + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + SectionTitle(text = "Checklist") + + ElevatedCard( + shape = RoundedCornerShape(24.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + if (items.isEmpty()) { + EmptyStateMessage(text = "No items yet. Add one below!") + } else { + items.forEach { item -> + ExpressiveChecklistItem( + text = item.text, + isChecked = item.isChecked, + onCheckedChange = { isChecked -> + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) + onEvent(PhotoDoDetailEvent.OnItemCheckedChange(item, isChecked)) + }, + onDelete = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + onEvent(PhotoDoDetailEvent.OnDeleteItem(item)) + } + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } - // --- 1. EXPRESSIVE STATE MANAGEMENT --- + Spacer(modifier = Modifier.height(16.dp)) + + // --- NEW ITEM INPUT --- + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + value = newItemText, + onValueChange = { newItemText = it }, + placeholder = { Text("Add new task...") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(16.dp), + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + if (newItemText.isNotBlank()) { + onEvent(PhotoDoDetailEvent.OnAddItemClicked(newItemText)) + newItemText = "" + } + }), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) + + Spacer(Modifier.width(12.dp)) + + FilledIconButton( + onClick = { + if (newItemText.isNotBlank()) { + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) + onEvent(PhotoDoDetailEvent.OnAddItemClicked(newItemText)) + newItemText = "" + } + }, + modifier = Modifier.size(48.dp) + ) { + Icon(Icons.Default.Add, contentDescription = "Add Item") + } + } + } + } + } + + // --- 3. PHOTOS SECTION (UPDATED UI) --- + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + SectionTitle(text = "Photos") + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 4.dp, vertical = 8.dp) + ) { + // Add Photo Button + item { + ExpressiveAddPhotoCard( + onClick = { + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) + // Correctly calls the event from your system code + onEvent(PhotoDoDetailEvent.OnCameraClick) + } + ) + } + // Photo List + items(photos, key = { it.photoId }) { photo -> + PhotoItem( + photo = photo, + onDeleteClick = { + // Correctly calls the event from your system code + onEvent(PhotoDoDetailEvent.OnDeletePhoto(photo.photoId)) + } + ) + } + } + } + + Spacer(Modifier.height(80.dp)) + } +} + +// ========================================== +// SUB-COMPONENTS +// ========================================== + +@Composable +fun PhotoItem( + photo: PhotoEntity, + onDeleteClick: () -> Unit +) { + Box( + modifier = Modifier + .size(100.dp) + .clip(RoundedCornerShape(20.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer) + ) { + // Placeholder for image loading (e.g. Coil) + Icon( + imageVector = Icons.Default.Image, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f), + modifier = Modifier.align(Alignment.Center) + ) + + // Delete Button Overlay + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(4.dp) + .size(24.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.7f)) + .clickable(onClick = onDeleteClick), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Delete photo", + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + } + } +} + +@Composable +fun HeroHeaderCard(title: String, notes: String?, onEditClick: () -> Unit) { val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() - val haptics = LocalHapticFeedback.current - // Spring Physics: Bouncy scale effect on press val scale by animateFloatAsState( - targetValue = if (isPressed) 0.97f else 1f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioLowBouncy, - stiffness = Spring.StiffnessMedium - ), - label = "CardScale" - ) - - // Shape Morph: Corners get rounder when pressed - val cornerRadius by animateIntAsState( - targetValue = if (isPressed) 32 else 24, // Morph from Large to Extra Large - animationSpec = spring(stiffness = Spring.StiffnessLow), - label = "CornerMorph" + targetValue = if (isPressed) 0.98f else 1f, + label = "heroScale" ) - // Color: Subtle tint shift - val containerColor by animateColorAsState( - targetValue = if (isPressed) - MaterialTheme.colorScheme.surfaceContainerHigh - else - MaterialTheme.colorScheme.surfaceContainer, - label = "ColorShift" - ) - - // -------------------------------------- - ElevatedCard( - modifier = modifier + modifier = Modifier .fillMaxWidth() - .scale(scale) // Apply physics scale - .clickable( - interactionSource = interactionSource, - indication = null // Disable default ripple for cleaner physics feel - ) { - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - // Optional: Navigate to full edit screen or expand - }, - shape = RoundedCornerShape(cornerRadius.dp), - colors = CardDefaults.elevatedCardColors(containerColor = containerColor), - elevation = CardDefaults.elevatedCardElevation( - defaultElevation = if (isPressed) 2.dp else 6.dp // Depress effect - ) + .scale(scale) + .clickable(interactionSource = interactionSource, indication = null) { }, + shape = RoundedCornerShape(28.dp), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = 4.dp) ) { - Column( - modifier = Modifier.padding(24.dp) // Generous "Expressive" spacing - ) { - // --- HEADER ROW --- + Column(Modifier.padding(24.dp)) { Row( verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.SpaceBetween, @@ -119,38 +271,20 @@ fun DetailCard( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = taskList.name, + text = title, style = MaterialTheme.typography.headlineMedium.copy( - fontWeight = FontWeight.ExtraBold, // Bolder is better - letterSpacing = (-0.5).sp // Tighter tracking for headlines + fontWeight = FontWeight.ExtraBold, + letterSpacing = (-0.5).sp ), - color = MaterialTheme.colorScheme.onSurface, - maxLines = 2, - overflow = TextOverflow.Ellipsis + color = MaterialTheme.colorScheme.onSurface ) - Spacer(modifier = Modifier.height(4.dp)) - // if (!taskList.name.isNullOrBlank()) { - Text( - text = "Description",//taskList.discription ?: "", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - //} } - - // Action Button (Edit) IconButton( - onClick = { - haptics.performHapticFeedback(HapticFeedbackType.ContextClick) - // onEvent(PhotoDoDetailEvent.OnEditList(taskList)) - }, - modifier = Modifier - .background( - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), - CircleShape - ) + onClick = onEditClick, + modifier = Modifier.background( + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), + CircleShape + ) ) { Icon( imageVector = Icons.Default.Edit, @@ -160,122 +294,126 @@ fun DetailCard( } } - Spacer(modifier = Modifier.height(24.dp)) - - // --- PROGRESS SECTION --- - // Calculate progress (e.g., if you have a 'total' target, or just count photos) - // Assuming simplified logic: 1 photo = 10% progress for visual demo, - // or modify 'TaskListEntity' to have a 'targetPhotoCount'. - val photoCount = photos.size - val targetCount = 10 // Arbitrary target for visualization - val progress = (photoCount / targetCount.toFloat()).coerceIn(0f, 1f) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { + if (!notes.isNullOrBlank()) { + Spacer(Modifier.height(12.dp)) Text( - text = "$photoCount Photos", - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.primary - ) - Text( - text = "Goal: $targetCount", - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.outline + text = notes, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } + } + } +} - Spacer(modifier = Modifier.height(8.dp)) +@Composable +fun ExpressiveChecklistItem( + text: String, + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, + onDelete: () -> Unit +) { + val backgroundColor by animateColorAsState( + targetValue = if (isChecked) MaterialTheme.colorScheme.surfaceContainerHigh else MaterialTheme.colorScheme.surface, + label = "rowColor" + ) - LinearProgressIndicator( - progress = { progress }, - modifier = Modifier - .fillMaxWidth() - .height(12.dp) // Thicker, expressive bar - .clip(RoundedCornerShape(50)), // Pill shape - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.surfaceContainerHighest, - strokeCap = StrokeCap.Round, + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(backgroundColor) + .clickable { onCheckedChange(!isChecked) } + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = isChecked, + onCheckedChange = null, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.primary, + uncheckedColor = MaterialTheme.colorScheme.outline ) - - Spacer(modifier = Modifier.height(24.dp)) - - // --- EXPRESSIVE ACTIONS ROW --- - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - // "Add Photo" Chip - High Emphasis - ExpressiveActionButton( - text = "Add Photo", - icon = Icons.Default.Add, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.weight(1f), - onClick = {}// onEvent(PhotoDoDetailEvent.OnPhotoSaved(taskList.listId)) } - ) - - // "View Gallery" Chip - Medium Emphasis - ExpressiveActionButton( - text = "Gallery", - icon = Icons.Default.PhotoLibrary, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier.weight(1f), - onClick = { /* Navigate to gallery view */ } - ) - } + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = text, + style = MaterialTheme.typography.bodyLarge.copy( + textDecoration = if (isChecked) TextDecoration.LineThrough else null + ), + color = if (isChecked) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + IconButton( + onClick = onDelete, + modifier = Modifier.size(32.dp), + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.error.copy(alpha = 0.7f) + ) + ) { + Icon(Icons.Default.Delete, contentDescription = "Delete", modifier = Modifier.size(20.dp)) } } } -/** - * A custom helper button that matches the Expressive style: - * - Tall height (56dp) - * - Fully rounded corners - */ @Composable -private fun ExpressiveActionButton( - text: String, - icon: androidx.compose.ui.graphics.vector.ImageVector, - containerColor: Color, - contentColor: Color, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - val haptics = LocalHapticFeedback.current +fun ExpressiveAddPhotoCard(onClick: () -> Unit) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.95f else 1f, + label = "scale" + ) Box( - modifier = modifier - .height(56.dp) - .clip(RoundedCornerShape(16.dp)) // Medium rounded - .background(containerColor) - .clickable { - haptics.performHapticFeedback(HapticFeedbackType.ContextClick)//.LightImpact) - onClick() - }, + modifier = Modifier + .size(100.dp) + .scale(scale) + .clip(RoundedCornerShape(20.dp)) + .background(MaterialTheme.colorScheme.primaryContainer) + .clickable(interactionSource = interactionSource, indication = null) { onClick() }, contentAlignment = Alignment.Center ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon( - imageVector = icon, - contentDescription = null, - tint = contentColor, - modifier = Modifier.size(20.dp) + imageVector = Icons.Default.CameraAlt, + contentDescription = "Take Photo", + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(32.dp) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( - text = text, - style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold), - color = contentColor + text = "Add", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer ) } } } -/* Extension for convenient SP unit if not available -private val Int.sp: androidx.compose.ui.unit.TextUnit - get() = androidx.compose.ui.unit.sp(this) -private val Double.sp: androidx.compose.ui.unit.TextUnit - get() = androidx.compose.ui.unit.sp(this)*/ \ No newline at end of file +@Composable +fun SectionTitle(text: String) { + Text( + text = text, + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 4.dp, bottom = 4.dp) + ) +} + +@Composable +fun EmptyStateMessage(text: String) { + Box( + modifier = Modifier.fillMaxWidth().padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} \ No newline at end of file From aade4b8354a724a0692b22b718737ed551a54ff7 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 4 Dec 2025 18:34:06 -0800 Subject: [PATCH 008/307] refactor(detail): Replace 'Add Photo' card with section header button This commit refactors the UI for adding photos in the `DetailCard` composable. The large, expressive "Add Photo" card has been removed from the photo list and replaced with a more conventional `IconButton` (camera icon) located in the section header. This change streamlines the layout, making it cleaner and more consistent with standard UI patterns. **Key Changes:** * **`DetailCard.kt`**: * The `ExpressiveAddPhotoCard` composable has been completely removed. * The "Photos" section now features a `Row` containing the "Photos" title and an `IconButton` to trigger the `OnCameraClick` event. * The large `ExpressiveAddPhotoCard` item has been removed from the `LazyRow` of photos. * `SectionTitle` now accepts an optional `Modifier`. * **`PhotoDoDetailViewModel.kt`**: * A placeholder handler for a new `OnEditList` event has been added. The business logic is commented out, indicating it is a work in progress. --- .../ui/detail/PhotoDoDetailViewModel.kt | 7 +- .../ui/detail/components/DetailCard.kt | 73 +++++++------------ 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/PhotoDoDetailViewModel.kt b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/PhotoDoDetailViewModel.kt index c505530f8..5e85cd125 100644 --- a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/PhotoDoDetailViewModel.kt +++ b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/PhotoDoDetailViewModel.kt @@ -184,7 +184,12 @@ class PhotoDoDetailViewModel @Inject constructor( } } - + is PhotoDoDetailEvent.OnEditList -> { + Log.d(TAG, "Editing task list: ${event.taskList.name}") + viewModelScope.launch { + //photoDoRepo.updateTaskList(event.taskList) + } + }//TODO() } } } \ No newline at end of file diff --git a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt index 99c0060d2..0e3457d7d 100644 --- a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt +++ b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt @@ -168,22 +168,33 @@ fun DetailCard( // --- 3. PHOTOS SECTION (UPDATED UI) --- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - SectionTitle(text = "Photos") + // New Header Row with Add Button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + SectionTitle(text = "Photos", modifier = Modifier.padding(bottom = 0.dp)) + IconButton( + onClick = { + haptics.performHapticFeedback(HapticFeedbackType.ContextClick) + onEvent(PhotoDoDetailEvent.OnCameraClick) + } + ) { + Icon( + imageVector = Icons.Default.CameraAlt, + contentDescription = "Add Photo", + tint = MaterialTheme.colorScheme.primary + ) + } + } LazyRow( horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(horizontal = 4.dp, vertical = 8.dp) ) { - // Add Photo Button - item { - ExpressiveAddPhotoCard( - onClick = { - haptics.performHapticFeedback(HapticFeedbackType.ContextClick) - // Correctly calls the event from your system code - onEvent(PhotoDoDetailEvent.OnCameraClick) - } - ) - } + // REMOVED: The large "Add Photo" card item is gone. + // Photo List items(photos, key = { it.photoId }) { photo -> PhotoItem( @@ -358,49 +369,15 @@ fun ExpressiveChecklistItem( } } -@Composable -fun ExpressiveAddPhotoCard(onClick: () -> Unit) { - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.95f else 1f, - label = "scale" - ) - - Box( - modifier = Modifier - .size(100.dp) - .scale(scale) - .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.primaryContainer) - .clickable(interactionSource = interactionSource, indication = null) { onClick() }, - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = Icons.Default.CameraAlt, - contentDescription = "Take Photo", - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.size(32.dp) - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "Add", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } -} +// REMOVED: ExpressiveAddPhotoCard is no longer needed. @Composable -fun SectionTitle(text: String) { +fun SectionTitle(text: String, modifier: Modifier = Modifier) { Text( text = text, style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 4.dp, bottom = 4.dp) + modifier = modifier.padding(start = 4.dp, bottom = 4.dp) ) } From ceb0240b9f1a719221ed2cc1833af3b20b512bba Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 5 Dec 2025 15:22:01 -0800 Subject: [PATCH 009/307] refactor(home): Centralize navigation and layout logic in ViewModel This commit refactors the home screen's architecture to handle layout changes and navigation more robustly across different screen sizes (phone vs. tablet). The `HomeViewModel` is now the central authority for screen state, and navigation is managed via a clean, side-effect-based approach. Previously, navigation and layout logic were scattered, with the UI making decisions based on `isExpandedScreen`. Now, the UI informs the ViewModel of the layout, and the ViewModel sends back one-time navigation commands (`HomeNavigationEffect`), which the UI then executes. This change decouples the ViewModel from the navigation implementation and creates a more predictable, state-driven flow. **Key Changes:** * **`HomeViewModel.kt`**: * A new state property `_isExpandedScreen` tracks the current layout configuration. * Introduced a `Channel` for one-time `HomeNavigationEffect` side-effects. This is used to command the UI to navigate (e.g., to the detail pane) without embedding navigation logic directly in the ViewModel. * **`PhotoDoHomeUiRoute.kt`**: * Uses a `LaunchedEffect` to notify the ViewModel of screen size changes via the new `OnScreenLayoutChanged` event. * On non-expanded screens (phones), it now collects `navigationEffect` and triggers the appropriate navigation controller (`navTo(listId)`). * Intercepts `OnEditCategoryClicked` events to route them up to the main screen, while passing all other events to the `HomeViewModel`. * Passes the `navigationEffect` flow down to the `HomeScreen`. * **`HomeScreen.kt`**: * Now relies on `uiState.isExpandedScreen` from the ViewModel as the single source of truth for its layout. * The `onSelectList` callback is refactored to emit a standard `HomeEvent.OnTaskListSelected`. * On expanded screens, it uses a `LaunchedEffect` to listen for `HomeNavigationEffect.NavigateToDetailPane` and correctly uses the `ListDetailPaneScaffoldNavigator` to show the detail pane. * **`HomeEvent.kt` & `HomeUiState.kt`**: * Added `HomeEvent.OnScreenLayoutChanged` to allow the UI to report layout changes. * Added `isExpandedScreen: Boolean` to `HomeUiState` to make the layout state available to all UI components. * **`HomeNavigationEffect.kt`**: * A new sealed interface to define one-time navigation events, starting with `NavigateToDetailPane`. --- .../photodo/features/home/ui/HomeEvent.kt | 4 ++ .../photodo/features/home/ui/HomeScreen.kt | 41 ++++++++----- .../photodo/features/home/ui/HomeUiState.kt | 3 +- .../photodo/features/home/ui/HomeViewModel.kt | 19 ++++++- .../features/home/ui/PhotoDoHomeUiRoute.kt | 57 +++++++++++++++---- 5 files changed, 96 insertions(+), 28 deletions(-) diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeEvent.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeEvent.kt index df6d89266..10f46780b 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeEvent.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeEvent.kt @@ -56,4 +56,8 @@ sealed interface HomeEvent { data class OnDeleteCategoryClicked(val category: CategoryEntity) : HomeEvent data class OnTaskListSelected(val listId: Long) : HomeEvent + // data class OnTaskListDeleted(val listId: Long) : HomeEvent + + // Navigation & Layout (The missing pieces) + data class OnScreenLayoutChanged(val isExpanded: Boolean) : HomeEvent } diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeScreen.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeScreen.kt index 714fbd4b8..c96df66e4 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeScreen.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeScreen.kt @@ -13,7 +13,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import com.ylabz.basepro.applications.photodo.features.home.ui.components.CategoryList import com.ylabz.basepro.applications.photodo.features.home.ui.components.TaskList -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -22,9 +22,7 @@ fun HomeScreen( modifier: Modifier = Modifier, uiState: HomeUiState.Success, onEvent: (HomeEvent) -> Unit, - // This is for navigating from a Task in the list to the Task Detail (3rd pane) - onSelectList: (Long) -> Unit, - isExpandedScreen: Boolean, + navigationEffects: Flow // Pass the side-effect flow //onCategorySelected: (Long) -> Unit, // <-- ADD THIS PARAMETER // ### WHAT: This parameter was added to accept the "setter" function from MainScreen. // ### WHY: This allows HomeScreen to tell MainScreen which FAB to display. @@ -37,7 +35,7 @@ fun HomeScreen( Log.d("HomeScreen", "On HomeScreen recomposing.") - if (isExpandedScreen) { + if (uiState.isExpandedScreen) { // val navigator = rememberListDetailPaneScaffoldNavigator() val navigator = rememberListDetailPaneScaffoldNavigator() @@ -46,14 +44,26 @@ fun HomeScreen( // --- ADAPTIVE NAVIGATION SYNC --- // Automatically navigate to the Detail pane when a category is selected. // This ensures the UI stays in sync with your ViewModel state. + // --- EFFECT HANDLING --- + // Listen for "Go To Detail" commands from the ViewModel + LaunchedEffect(navigationEffects) { + navigationEffects.collect { effect -> + when (effect) { + is HomeNavigationEffect.NavigateToDetailPane -> { + // Navigate to the detail pane (right side) + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) + // Note: If you need to navigate to a NEW screen (Screen C), + // you would trigger a callback to the MainActivity here. + } + } + } + } + + // --- SYNC STATE --- + // Ensure navigator aligns with selected category LaunchedEffect(uiState.selectedCategory) { if (uiState.selectedCategory != null) { - scope.launch { - navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) - } - } else { - // Optional: If deselected, you could navigate back to List - // scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.List) } + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) } } @@ -68,7 +78,7 @@ fun HomeScreen( CategoryList( uiState = uiState, onEvent = onEvent, - isExpandedScreen = isExpandedScreen, + isExpandedScreen = true, ) } }, @@ -84,7 +94,10 @@ fun HomeScreen( // This is for clicking items in the right-hand pane, which is already correct // This onSelectList is for when a user clicks a task *within* this list, // which correctly triggers the navigation to the 3rd pane. - onSelectList = onSelectList + // Pure Event Passing + onSelectList = { listId -> + onEvent(HomeEvent.OnTaskListSelected(listId)) + } ) } } @@ -99,7 +112,7 @@ fun HomeScreen( modifier = modifier.fillMaxSize(), uiState = uiState, onEvent = onEvent, - isExpandedScreen = isExpandedScreen + isExpandedScreen = false ) } diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeUiState.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeUiState.kt index 2f8292077..4af0164ff 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeUiState.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeUiState.kt @@ -26,7 +26,8 @@ sealed interface HomeUiState { val taskListsForSelectedCategory: List = emptyList(), val isAddingCategory: Boolean = false, // --- Added Field --- - val categoryToEdit: CategoryEntity? = null + val categoryToEdit: CategoryEntity? = null, + val isExpandedScreen: Boolean = false ) : HomeUiState /** diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt index f3488819a..e2fa5c805 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt @@ -7,6 +7,7 @@ import com.ylabz.basepro.applications.photodo.db.entity.CategoryEntity import com.ylabz.basepro.applications.photodo.db.repo.PhotoDoRepo import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -41,6 +43,13 @@ class HomeViewModel @Inject constructor( private val _categorySelectedEvent = MutableSharedFlow() val categorySelectedEvent: SharedFlow = _categorySelectedEvent.asSharedFlow() + private val _isExpandedScreen = MutableStateFlow(false) // New + + // 2. One-Time Events (Side Effects) + // Used to tell the UI to Navigate (e.g. slide to detail pane) + private val _navigationEffect = Channel(Channel.BUFFERED) + val navigationEffect = _navigationEffect.receiveAsFlow() + // --- REACTIVE PIPELINE (Replaces loadInitialData) --- // A. The raw list of categories from the DB @@ -176,7 +185,9 @@ class HomeViewModel @Inject constructor( HomeEvent.OnAddListClicked -> { /* Handled by MainScreen */ } is HomeEvent.OnTaskListSelected -> { /* Navigation handled by UI Route */ } - + is HomeEvent.OnScreenLayoutChanged -> { + _isExpandedScreen.value = event.isExpanded + } } } } @@ -212,3 +223,9 @@ class HomeViewModel @Inject constructor( */ } } + + +// Helper Sealed Class for One-Off Navigation Events +sealed interface HomeNavigationEffect { + data class NavigateToDetailPane(val listId: Long) : HomeNavigationEffect +} diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/PhotoDoHomeUiRoute.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/PhotoDoHomeUiRoute.kt index 28089dcd5..badf6ce0f 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/PhotoDoHomeUiRoute.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/PhotoDoHomeUiRoute.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.ylabz.basepro.applications.photodo.db.entity.CategoryEntity @@ -35,7 +36,40 @@ fun PhotoDoHomeUiRoute( Log.d(TAG, "Entered PhotoDoHomeUiRoute composable") // <-- BREADCRUMB 1 + // 1. SYNC LAYOUT STATE + // Tell the ViewModel about the screen size so it can update HomeUiState + LaunchedEffect(isExpandedScreen) { + homeViewModel.onEvent(HomeEvent.OnScreenLayoutChanged(isExpandedScreen)) + } + + // 2. HANDLE PHONE NAVIGATION + // If we are NOT on an expanded screen, we listen for navigation effects here + // and trigger the external navigation controller. + if (!isExpandedScreen) { + LaunchedEffect(homeViewModel.navigationEffect) { + homeViewModel.navigationEffect.collect { effect -> + when (effect) { + is HomeNavigationEffect.NavigateToDetailPane -> { + Log.d(TAG, "Phone Navigation triggered for list: ${effect.listId}") + navTo(effect.listId) + } + } + } + } + } + + // 3. EVENT INTERCEPTOR + // We wrap the onEvent to intercept "Edit" actions (which need to go up to MainScreen), + // but pass everything else to the ViewModel. val onEvent: (HomeEvent) -> Unit = { event -> + if (event is HomeEvent.OnEditCategoryClicked) { + onEditCategory(event.category) + } else { + homeViewModel.onEvent(event) + } + } + + val onEventOld: (HomeEvent) -> Unit = { event -> // 1. Notify local VM (for selection, etc.) homeViewModel.onEvent(event) @@ -57,6 +91,7 @@ fun PhotoDoHomeUiRoute( // ------------------------ } + // 4. RENDER when (uiState) { is HomeUiState.Loading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -65,22 +100,20 @@ fun PhotoDoHomeUiRoute( } is HomeUiState.Success -> { Column(modifier = Modifier.fillMaxSize()) { - Text("PhotoDoHomeUI Holding the HomeScreen") + // Optional: Keep for debugging, remove for production + // Text("PhotoDoHomeUI Holding the HomeScreen") + HomeScreen( modifier = modifier, uiState = uiState, - onEvent = homeViewModel::onEvent, - onSelectList = { taskID -> navTo(taskID)}, - isExpandedScreen = isExpandedScreen, - // onCategorySelected = onCategorySelected, // <-- PASS THE LAMBDA DOWN - // This handles the "Add" button on the empty screen - /*onAddList = { - state.selectedCategory?.let { category -> - viewModel.onEvent(HomeEvent.OnAddTaskListClicked(category.categoryId)) - } - },*/ - // setFabState = setFabState + onEvent = onEvent, + // We pass the flow down. + // The HomeScreen will ONLY collect this if isExpandedScreen is true. + navigationEffects = homeViewModel.navigationEffect ) + + + // --- THIS IS THE UI LOGIC --- // When the ViewModel's state flag is true, show the sheet. /* if (uiState.isAddingCategory) { From ef3d959f57ad6614b891252d14696ceb8a1fe2fd Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 7 Dec 2025 13:13:00 -0800 Subject: [PATCH 010/307] chore: Update dependencies This commit updates various project dependencies to their latest versions. Key updates include: * Kotlin: `2.2.20` -> `2.2.21` * AndroidX Lifecycle: `2.9.3` -> `2.10.0` * AndroidX Navigation: `2.9.4` -> `2.9.6` * Navigation 3: `1.0.0-alpha10` -> `1.0.0` * Jetpack Compose BOM: `2025.09.00` -> `2025.12.00` * Hilt: `2.57.1` -> `2.57.2` * Room: `2.8.0` -> `2.8.4` * WorkManager: `2.10.4` -> `2.11.0` * Firebase BOM: `34.2.0` -> `34.6.0` * Other minor version bumps for libraries like CameraX, DataStore, OkHttp, and Maps Compose. Additionally, the `androidx.material.icons.extended` dependency has been added to the `alarm`, `ble`, `health`, `listings`, and `ui` feature modules. --- core/ui/build.gradle.kts | 2 ++ feature/alarm/build.gradle.kts | 1 + feature/ble/build.gradle.kts | 2 ++ feature/heatlh/build.gradle.kts | 1 + feature/listings/build.gradle.kts | 1 + gradle/libs.versions.toml | 44 +++++++++++++++---------------- 6 files changed, 29 insertions(+), 22 deletions(-) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 2af5bc6eb..4db717c33 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -47,6 +47,8 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + implementation(libs.androidx.material.icons.extended) + // Compose implementation(platform(libs.androidx.compose.bom)) diff --git a/feature/alarm/build.gradle.kts b/feature/alarm/build.gradle.kts index 16eac4786..022361169 100644 --- a/feature/alarm/build.gradle.kts +++ b/feature/alarm/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { // Compose implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) // Hilt Dependency Injection implementation(libs.hilt.navigation.compose) diff --git a/feature/ble/build.gradle.kts b/feature/ble/build.gradle.kts index 85474d3b6..351f15fdd 100644 --- a/feature/ble/build.gradle.kts +++ b/feature/ble/build.gradle.kts @@ -57,6 +57,8 @@ dependencies { // Compose implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) + // Hilt Dependency Injection implementation(libs.hilt.navigation.compose) diff --git a/feature/heatlh/build.gradle.kts b/feature/heatlh/build.gradle.kts index 4f7504bba..7b1cc88bb 100644 --- a/feature/heatlh/build.gradle.kts +++ b/feature/heatlh/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { // Compose implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) debugImplementation(libs.androidx.ui.tooling) // Hilt Dependency Injection diff --git a/feature/listings/build.gradle.kts b/feature/listings/build.gradle.kts index 01f090c1e..03ef219a9 100644 --- a/feature/listings/build.gradle.kts +++ b/feature/listings/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { // Compose implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) debugImplementation(libs.androidx.ui.tooling) // Hilt Dependency Injection diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5819748d4..b2a9580e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ targetSdk = "35" # Android Target SDK version minSdk = "34" # Android Minimum SDK version # Kotlin & Related Tools -kotlin = "2.2.20" # Kotlin programming language version +kotlin = "2.2.21" # Kotlin programming language version ksp = "2.2.20-2.0.3" # Kotlin Symbol Processing API version kotlinxSerializationCore = "1.9.0" # Kotlinx Serialization library version (used for core and json) immutableCollections = "0.4.0" # Kotlinx Immutable Collections library version @@ -22,42 +22,42 @@ coreKtx = "1.17.0" # AndroidX Core KTX, Kotlin extensions for Andro appCompat = "1.7.1" # AndroidX AppCompat, for backward-compatible versions of Android components # AndroidX - Lifecycle -lifecycle = "2.9.3" # AndroidX Lifecycle components (ViewModel, LiveData, Runtime) -lifecycleService = "2.9.3" # AndroidX Lifecycle Service component +lifecycle = "2.10.0" # AndroidX Lifecycle components (ViewModel, LiveData, Runtime) +lifecycleService = "2.10.0" # AndroidX Lifecycle Service component # AndroidX - Activity -activityCompose = "1.11.0" # AndroidX Activity Compose, for Jetpack Compose in Activities +activityCompose = "1.12.1" # AndroidX Activity Compose, for Jetpack Compose in Activities # AndroidX - Navigation (old and we need to remove) -androidxNavigation = "2.9.4" # AndroidX Navigation component (Compose) +androidxNavigation = "2.9.6" # AndroidX Navigation component (Compose) # # AndroidX - Navigation (Nav3) -nav3Core = "1.0.0-alpha10" # Navigation 3 Core libraries version -lifecycleViewmodelNav3 = "2.10.0-alpha04" # Navigation 3 ViewModel support -material3AdaptiveNav3 = "1.0.0-alpha03" # Navigation 3 Material3 Adaptive Layouts (Snapshot) +nav3Core = "1.0.0" # Navigation 3 Core libraries version +lifecycleViewmodelNav3 = "2.10.0" # Navigation 3 ViewModel support +material3AdaptiveNav3 = "1.3.0-alpha05" # Navigation 3 Material3 Adaptive Layouts (Snapshot) # material3Adaptive = "1.0.0-alpha06" # AndroidX Test Runner # Jetpack Compose -composeBom = "2025.09.00" # Jetpack Compose Bill of Materials (BOM) +composeBom = "2025.12.00" # Jetpack Compose Bill of Materials (BOM) composeMaterial = "1.7.8" # Jetpack Compose Material (used for icons, wear material) # Note: androidx.compose.material3 uses the BoM version. # Note: androidx.compose.ui:ui-tooling-preview uses the BoM version. -composeMaterial3Expressive = "1.5.0-alpha04" # AndroidX Test Runner +composeMaterial3Expressive = "1.5.0-alpha10" # AndroidX Test Runner # Dependency Injection - Hilt -hilt = "2.57.1" # Hilt, a dependency injection library for Android +hilt = "2.57.2" # Hilt, a dependency injection library for Android hiltLifecycleViewModel = "1.3.0" # Hilt AndroidX Lifecycle ViewModel integration hiltNavigationCompose = "1.3.0" # Hilt Jetpack Compose Navigation integration hiltWork = "1.3.0" # Hilt WorkManager integration # Data Persistence -room = "2.8.0" # AndroidX Room, an abstraction layer over SQLite -datastore = "1.1.7" # AndroidX DataStore, for persistent data storage +room = "2.8.4" # AndroidX Room, an abstraction layer over SQLite +datastore = "1.2.0" # AndroidX DataStore, for persistent data storage # Background Processing -work = "2.10.4" # AndroidX WorkManager, for background tasks +work = "2.11.0" # AndroidX WorkManager, for background tasks # Networking -okhttp = "5.1.0" # Square OkHttp, HTTP client +okhttp = "5.3.2" # Square OkHttp, HTTP client squareupRetrofit = "3.0.0" # Square Retrofit, type-safe HTTP client for Android and Java # UI Components & Utilities @@ -69,23 +69,23 @@ coil = "2.7.0" # Coil, an image loading library for Android accompanistPermissions = "0.37.3" # Accompanist Permissions library # Camera -cameraX = "1.5.1" # AndroidX CameraX libraries +cameraX = "1.5.2" # AndroidX CameraX libraries # Maps & Location -mapsCompose = "6.10.0" # Maps Compose library for Jetpack Compose +mapsCompose = "6.12.2" # Maps Compose library for Jetpack Compose mapsplatformSecretsPlugin = "2.0.1" # Gradle plugin for Maps API key secrets playServicesMaps = "19.2.0" # Google Play Services Maps SDK playServicesLocation = "21.3.0" # Google Play Services Location SDK # Health Connect -healthConnect = "1.1.0-rc03" # Health Connect client library +healthConnect = "1.1.0" # Health Connect client library # Wear OS playServicesWearable = "19.0.0" # Google Play Services for Wear OS wearComposeMaterial = "1.7.8" # Wear OS Jetpack Compose Material library (used by icons too) -wearComposeMaterial3 = "1.5.1" # Wear OS Jetpack Compose Material3 library +wearComposeMaterial3 = "1.5.6" # Wear OS Jetpack Compose Material3 library wearToolingPreview = "1.0.0" # Wear OS Tooling Preview library -coreSplashscreen = "1.0.1" # AndroidX Core Splashscreen library +coreSplashscreen = "1.2.0" # AndroidX Core Splashscreen library tiles = "1.5.0" # Wear OS Tiles libraries (tiles, tiles-material, tiles-tooling, tiles-tooling-preview) horologist = "0.7.15" # Horologist libraries for Wear OS watchfaceComplicationsDataSourceKtx = "1.2.1" # Wear OS Watchface Complications KTX library @@ -98,8 +98,8 @@ mlkitTextRecognition = "16.0.1" # ML Kit Text Recognition library playServicesCodeScanner = "16.1.0" # Google Play Services Code Scanner (QR Scanner) # Firebase -firebaseBom = "34.2.0" # Firebase Bill of Materials (BOM) -googleGmsGoogleServices = "4.4.3" # Google Services Gradle plugin +firebaseBom = "34.6.0" # Firebase Bill of Materials (BOM) +googleGmsGoogleServices = "4.4.4" # Google Services Gradle plugin googleFirebaseCrashlytics = "3.0.6" # Firebase Crashlytics Gradle plugin # GraphQL (Apollo) From 4ec6558620d352b955012eaa7f815f87a93b2905 Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 7 Dec 2025 13:17:57 -0800 Subject: [PATCH 011/307] feat(home): Add debug previews for CategoryCard and TaskList This commit introduces new debug-only preview files to facilitate the UI development of the `CategoryCard` and `TaskList` components. These previews cover various states, such as selected, unselected, populated, and empty, ensuring components are visually correct under different conditions and on both light and dark themes. **Key Changes:** * **`CategoryCardPreviewNew.kt`**: * Adds multiple `@Preview` composables for the `CategoryCard`. * Includes previews for selected and unselected states, a column layout to test stacking, and a dark mode variant. * **`TaskListPreviews.kt`**: * Adds `@Preview` composables for the `TaskList` component. * Includes previews for a populated list, an empty state, and a state where no category is selected. --- .../ui/components/CategoryCardPreviewNew.kt | 125 ++++++++++++++++++ .../home/ui/components/TaskListPreviews.kt | 66 +++++++++ 2 files changed, 191 insertions(+) create mode 100644 applications/photodo/features/home/src/debug/kotlin/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCardPreviewNew.kt create mode 100644 applications/photodo/features/home/src/debug/kotlin/com/ylabz/basepro/applications/photodo/features/home/ui/components/TaskListPreviews.kt diff --git a/applications/photodo/features/home/src/debug/kotlin/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCardPreviewNew.kt b/applications/photodo/features/home/src/debug/kotlin/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCardPreviewNew.kt new file mode 100644 index 000000000..5d6fb54f1 --- /dev/null +++ b/applications/photodo/features/home/src/debug/kotlin/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCardPreviewNew.kt @@ -0,0 +1,125 @@ +package com.ylabz.basepro.applications.photodo.features.home.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ylabz.basepro.applications.photodo.db.entity.CategoryEntity +import com.ylabz.basepro.applications.photodo.db.entity.TaskListEntity + +// --- MOCK DATA --- +private val mockCategory = CategoryEntity( + categoryId = 1L, + name = "Kitchen Renovation", + description = "Cabinets, flooring, and paint selection for the new look.", + imageUri = null // Using null to trigger the placeholder icon +) + + +val sampleTasks = listOf( + TaskListEntity( + listId = 101L, + categoryId = 2L, + name = "Q4 Planning", + status = "To-Do", + priority = 1 + ), + TaskListEntity( + listId = 102L, + categoryId = 2L, + name = "Expense Report", + status = "Done", + priority = 0 + ) +) +private val mockTaskLists = listOf( + TaskListEntity(listId = 1L, categoryId = 2L,name = "Buy Paint", status = "Active", priority = 1), // High Priority + TaskListEntity(listId = 2L, categoryId = 2L,name = "Sand Walls", status = "Active", priority = 0), + TaskListEntity(listId = 3L, categoryId = 2L,name = "Install Handles", status = "Done", priority = 0) +) + +// --- PREVIEW --- + +@Preview(showBackground = true, name = "1. Selected State") +@Composable +fun PreviewCategoryCardSelected() { + MaterialTheme { + Surface(modifier = Modifier.padding(16.dp)) { + CategoryCard( + uiState = CategoryCardUiState( + category = mockCategory, + isSelected = true, + taskLists = mockTaskLists + ), + onEvent = {} + ) + } + } +} + +@Preview(showBackground = true, name = "2. Unselected State") +@Composable +fun PreviewCategoryCardUnselected() { + MaterialTheme { + Surface(modifier = Modifier.padding(16.dp)) { + CategoryCard( + uiState = CategoryCardUiState( + category = mockCategory.copy(name = "Groceries", description = "Weekly run"), + isSelected = false, + taskLists = emptyList() + ), + onEvent = {} + ) + } + } +} + +@Preview(showBackground = true, name = "3. Expanded State Logic Check") +@Composable +fun PreviewColumnDisplay() { + // This preview stacks them to see how they look in a list context + MaterialTheme { + Column(modifier = Modifier.padding(16.dp)) { + CategoryCard( + uiState = CategoryCardUiState( + category = mockCategory, + isSelected = true, + taskLists = mockTaskLists + ), + onEvent = {} + ) + Spacer(modifier = Modifier.height(16.dp)) + CategoryCard( + uiState = CategoryCardUiState( + category = mockCategory.copy(categoryId = 2L, name = "Backyard"), + isSelected = false, + taskLists = emptyList() + ), + onEvent = {} + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF1C1B1F, name = "4. Dark Mode") +@Composable +fun PreviewCategoryCardDark() { + MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { + Surface(modifier = Modifier.padding(16.dp)) { + CategoryCard( + uiState = CategoryCardUiState( + category = mockCategory, + isSelected = true, + taskLists = mockTaskLists + ), + onEvent = {} + ) + } + } +} \ No newline at end of file diff --git a/applications/photodo/features/home/src/debug/kotlin/com/ylabz/basepro/applications/photodo/features/home/ui/components/TaskListPreviews.kt b/applications/photodo/features/home/src/debug/kotlin/com/ylabz/basepro/applications/photodo/features/home/ui/components/TaskListPreviews.kt new file mode 100644 index 000000000..05e8cd282 --- /dev/null +++ b/applications/photodo/features/home/src/debug/kotlin/com/ylabz/basepro/applications/photodo/features/home/ui/components/TaskListPreviews.kt @@ -0,0 +1,66 @@ +package com.ylabz.basepro.applications.photodo.features.home.ui.components + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.ylabz.basepro.applications.photodo.db.entity.CategoryEntity +import com.ylabz.basepro.applications.photodo.db.entity.TaskListEntity + +// --- MOCK DATA --- +private val mockCategoryForList = CategoryEntity( + categoryId = 1L, + name = "Home Improvements", + description = "Things to fix around the house", + imageUri = null +) + +private val mockTaskListsData = listOf( + TaskListEntity(listId = 1L, categoryId = 2L,name = "Buy Paint", status = "Active", priority = 1), // High Priority + TaskListEntity(listId = 2L, categoryId = 2L,name = "Sand Walls", status = "Active", priority = 0), + TaskListEntity(listId = 3L, categoryId = 2L,name = "Install Handles", status = "Done", priority = 0) +) + +// --- PREVIEWS --- + +@Preview(showBackground = true, name = "1. Populated List") +@Composable +fun PreviewTaskListPopulated() { + MaterialTheme { + Surface { + TaskList( + category = mockCategoryForList, + taskLists = mockTaskListsData, + onSelectList = {} + ) + } + } +} + +@Preview(showBackground = true, name = "2. Empty State (No Lists)") +@Composable +fun PreviewTaskListEmpty() { + MaterialTheme { + Surface { + TaskList( + category = mockCategoryForList, + taskLists = emptyList(), // Simulating empty DB result + onSelectList = {} + ) + } + } +} + +@Preview(showBackground = true, name = "3. No Category Selected") +@Composable +fun PreviewTaskListNoCategory() { + MaterialTheme { + Surface { + TaskList( + category = null, // Simulating null selection + taskLists = emptyList(), + onSelectList = {} + ) + } + } +} \ No newline at end of file From 9668a51bf3819bc6e7ea71be1c58773be81e098a Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 7 Dec 2025 13:19:46 -0800 Subject: [PATCH 012/307] refactor(nav): Simplify `rememberNavBackStack` call by removing redundant type This commit removes the explicit `` generic type argument from calls to `rememberNavBackStack`. The type can be inferred by the compiler, making the code slightly cleaner and more concise. **Key Changes:** * **`MainScreen.kt`**: * Simplified `rememberNavBackStack(...)` to `rememberNavBackStack(...)`. * **`PhotoDoAppNav3.kt`**: * Simplified `rememberNavBackStack(...)` to `rememberNavBackStack(...)`. * Removed the now-unused `@Transient` annotation from the `AppScreen` sealed class. --- .../applications/photodo/ui/demo/navigation/PhotoDoAppNav3.kt | 3 +-- .../applications/photodo/ui/navigation/main/MainScreen.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/demo/navigation/PhotoDoAppNav3.kt b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/demo/navigation/PhotoDoAppNav3.kt index afdec0bf6..0b3665176 100644 --- a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/demo/navigation/PhotoDoAppNav3.kt +++ b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/demo/navigation/PhotoDoAppNav3.kt @@ -52,7 +52,6 @@ import kotlinx.serialization.Transient // --- STEP 1: DEFINE DESTINATIONS (UPDATED) --- @Serializable private sealed class AppScreen(val title: String) : NavKey { - @Transient abstract val icon: ImageVector @Serializable @@ -102,7 +101,7 @@ private fun SimpleAdaptiveBottomBar() { val isExpandedScreen = windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact // The back stack now holds the generic NavKey type to accommodate different screen types. - val backStack = rememberNavBackStack(AppScreen.Home) + val backStack = rememberNavBackStack(AppScreen.Home) var currentTab: AppScreen by rememberSaveable(stateSaver = AppScreenSaver) { mutableStateOf(AppScreen.Home) } diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt index 5ac3eaef7..fbd241147 100644 --- a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt +++ b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt @@ -83,7 +83,7 @@ fun MainScreen( // --- Navigation State --- // NOTE: **Bottom Navigation State** // The actual navigation history for the NavDisplay. - val backStack = rememberNavBackStack(PhotoDoNavKeys.HomeFeedKey) + val backStack = rememberNavBackStack(PhotoDoNavKeys.HomeFeedKey) // NOTE: **Adaptive Navigation State** val listDetailStrategy = rememberListDetailSceneStrategy() From 9c5b5afbb6133837032fdaafb868a20a11171955 Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 7 Dec 2025 13:20:09 -0800 Subject: [PATCH 013/307] fix(detail): Load photo thumbnails using Coil This commit replaces the placeholder `Icon` in the `PhotoItem` composable with an `AsyncImage` from the Coil library. This change enables the actual loading and display of photo thumbnails from the `photo.uri` in the photo management section of the detail screen. The image is configured to crop and fill the designated space. --- .../ui/detail/components/DetailCard.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt index 0e3457d7d..78e1fa2be 100644 --- a/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt +++ b/applications/photodo/features/photodolist/src/main/java/com/ylabz/basepro/applications/photodo/features/photodolist/ui/detail/components/DetailCard.kt @@ -32,7 +32,6 @@ import androidx.compose.material.icons.filled.CameraAlt import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Image import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults @@ -56,6 +55,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction @@ -63,6 +63,7 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage import com.ylabz.basepro.applications.photodo.db.entity.PhotoEntity import com.ylabz.basepro.applications.photodo.db.entity.TaskListWithPhotos import com.ylabz.basepro.applications.photodo.features.photodolist.ui.detail.PhotoDoDetailEvent @@ -227,13 +228,14 @@ fun PhotoItem( .clip(RoundedCornerShape(20.dp)) .background(MaterialTheme.colorScheme.surfaceContainer) ) { - // Placeholder for image loading (e.g. Coil) - Icon( - imageVector = Icons.Default.Image, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f), - modifier = Modifier.align(Alignment.Center) + // --- FIX: USE COIL TO LOAD THE IMAGE --- + AsyncImage( + model = photo.uri, // Assuming your PhotoEntity has a 'uri' string field + contentDescription = "Task photo", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() ) + // --------------------------------------- // Delete Button Overlay Box( From 29d8ce4d22b3fa04038ef5793ef9d472e31e77c3 Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 7 Dec 2025 13:20:30 -0800 Subject: [PATCH 014/307] fix(listings): Correct package name in DetailsRoute This commit corrects the package name in `DetailsRoute.kt` from `com.ylabz.basepro.listings.ui.components` to `com.ylabz.basepro.feature.listings.ui.components` to align with the project's directory structure. --- .../basepro/feature/listings/ui/components/DetailsRoute.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/listings/src/main/java/com/ylabz/basepro/feature/listings/ui/components/DetailsRoute.kt b/feature/listings/src/main/java/com/ylabz/basepro/feature/listings/ui/components/DetailsRoute.kt index 676e5c690..dc921e29d 100644 --- a/feature/listings/src/main/java/com/ylabz/basepro/feature/listings/ui/components/DetailsRoute.kt +++ b/feature/listings/src/main/java/com/ylabz/basepro/feature/listings/ui/components/DetailsRoute.kt @@ -1,4 +1,4 @@ -package com.ylabz.basepro.listings.ui.components +package com.ylabz.basepro.feature.listings.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box From aeb3e7b5630bfb77a3f6441794f9a122092f495e Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 7 Dec 2025 13:52:52 -0800 Subject: [PATCH 015/307] refactor(home): Remove navigation side-effect logic This commit removes the side-effect-based navigation handling from the home screen feature. The logic for observing `navigationEffect` in `PhotoDoHomeUiRoute` and `HomeScreen` has been commented out, indicating a shift away from this navigation pattern. This change also removes the passing of the `navigationEffect` flow from `PhotoDoHomeUiRoute` down to `HomeScreen`. **Key Changes:** * **`PhotoDoHomeUiRoute.kt`**: * The `LaunchedEffect` responsible for collecting navigation effects on phone layouts has been disabled. * The `navigationEffects` parameter is no longer passed to the `HomeScreen`. * **`HomeScreen.kt`**: * The `LaunchedEffect` that listened for `NavigateToDetailPane` effects on tablet layouts has been commented out. * The `navigationEffects` parameter has been removed from the function signature. --- .../applications/photodo/features/home/ui/HomeScreen.kt | 7 +++---- .../photodo/features/home/ui/HomeViewModel.kt | 8 +++++--- .../photodo/features/home/ui/PhotoDoHomeUiRoute.kt | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeScreen.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeScreen.kt index c96df66e4..62ae9b4b3 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeScreen.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import com.ylabz.basepro.applications.photodo.features.home.ui.components.CategoryList import com.ylabz.basepro.applications.photodo.features.home.ui.components.TaskList -import kotlinx.coroutines.flow.Flow @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -22,7 +21,7 @@ fun HomeScreen( modifier: Modifier = Modifier, uiState: HomeUiState.Success, onEvent: (HomeEvent) -> Unit, - navigationEffects: Flow // Pass the side-effect flow + //navigationEffects: Flow // Pass the side-effect flow //onCategorySelected: (Long) -> Unit, // <-- ADD THIS PARAMETER // ### WHAT: This parameter was added to accept the "setter" function from MainScreen. // ### WHY: This allows HomeScreen to tell MainScreen which FAB to display. @@ -46,7 +45,7 @@ fun HomeScreen( // This ensures the UI stays in sync with your ViewModel state. // --- EFFECT HANDLING --- // Listen for "Go To Detail" commands from the ViewModel - LaunchedEffect(navigationEffects) { + /*LaunchedEffect(navigationEffects) { navigationEffects.collect { effect -> when (effect) { is HomeNavigationEffect.NavigateToDetailPane -> { @@ -57,7 +56,7 @@ fun HomeScreen( } } } - } + }*/ // --- SYNC STATE --- // Ensure navigator aligns with selected category diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt index e2fa5c805..1aaf8277e 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt @@ -99,15 +99,17 @@ class HomeViewModel @Inject constructor( categoriesFlow, activeCategoryFlow, taskListsFlow, - _isAddingCategory - ) { categories, selectedCategory, taskLists, isAdding -> + _isAddingCategory, + _isExpandedScreen // <--- PASS IT IN HERE + ) { categories, selectedCategory, taskLists, isAdding,isExpanded -> // Map to Success state (or Loading if we wanted to add that logic) HomeUiState.Success( categories = categories, selectedCategory = selectedCategory, taskListsForSelectedCategory = taskLists, - isAddingCategory = isAdding + isAddingCategory = isAdding, + isExpandedScreen = isExpanded // <--- ASSIGN IT HERE ) }.stateIn( scope = viewModelScope, diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/PhotoDoHomeUiRoute.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/PhotoDoHomeUiRoute.kt index badf6ce0f..f4a407f99 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/PhotoDoHomeUiRoute.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/PhotoDoHomeUiRoute.kt @@ -45,7 +45,7 @@ fun PhotoDoHomeUiRoute( // 2. HANDLE PHONE NAVIGATION // If we are NOT on an expanded screen, we listen for navigation effects here // and trigger the external navigation controller. - if (!isExpandedScreen) { + // if (isExpandedScreen) { LaunchedEffect(homeViewModel.navigationEffect) { homeViewModel.navigationEffect.collect { effect -> when (effect) { @@ -56,7 +56,7 @@ fun PhotoDoHomeUiRoute( } } } - } + //} // 3. EVENT INTERCEPTOR // We wrap the onEvent to intercept "Edit" actions (which need to go up to MainScreen), @@ -109,7 +109,7 @@ fun PhotoDoHomeUiRoute( onEvent = onEvent, // We pass the flow down. // The HomeScreen will ONLY collect this if isExpandedScreen is true. - navigationEffects = homeViewModel.navigationEffect + // navigationEffects = homeViewModel.navigationEffect ) From 4b0a8c695eed2fa8a9e50ff1934ea1546fd4f973 Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 7 Dec 2025 14:50:45 -0800 Subject: [PATCH 016/307] feat(home): Navigate to detail pane on task list selection This commit updates the `HomeViewModel` to explicitly trigger navigation when a task list is selected. Previously, the `OnTaskListSelected` event was a no-op in the ViewModel, with navigation being handled implicitly by the UI layer. This change centralizes the navigation logic by having the ViewModel send a `NavigateToDetailPane` effect. The UI will then observe this effect and perform the actual navigation. This improves the separation of concerns by making the ViewModel responsible for *what* should happen (navigation), while the UI remains responsible for *how* it happens. **Key Changes:** * **`HomeViewModel.kt`**: * The `OnTaskListSelected` event handler now sends a `HomeNavigationEffect.NavigateToDetailPane` event, passing the selected `listId`. --- .../applications/photodo/features/home/ui/HomeViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt index 1aaf8277e..f10ce3e5f 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/HomeViewModel.kt @@ -186,7 +186,10 @@ class HomeViewModel @Inject constructor( HomeEvent.OnAddListClicked -> { /* Handled by MainScreen */ } - is HomeEvent.OnTaskListSelected -> { /* Navigation handled by UI Route */ } + is HomeEvent.OnTaskListSelected -> { + /* Navigation handled by UI Route */ + _navigationEffect.send(HomeNavigationEffect.NavigateToDetailPane(event.listId)) + } is HomeEvent.OnScreenLayoutChanged -> { _isExpandedScreen.value = event.isExpanded } From 419a960f8b8157be4d4f757287981f613a8b0d94 Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 7 Dec 2025 15:46:55 -0800 Subject: [PATCH 017/307] fix(ui): Remove padding from main app content to allow edge-to-edge display This commit removes the padding applied to the main `appContent` composable within the `MainScreen`. By commenting out `Modifier.padding(padding)` and replacing it with `Modifier.fillMaxSize()`, the content area is now allowed to draw edge-to-edge, behind system bars like the status and navigation bars. This change is typically made to support immersive or modern UI layouts. --- .../applications/photodo/ui/navigation/main/MainScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt index fbd241147..9c55cbb93 100644 --- a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt +++ b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt @@ -365,7 +365,8 @@ fun MainScreen( currentListId = currentListId, currentFabState = currentFabState ) - appContent(Modifier.padding(padding)) + // appContent(Modifier.padding(padding)) + appContent(Modifier.fillMaxSize()) } } } From 84fdf870585f4170f5ba41143a9dda42dd4c17c0 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 8 Dec 2025 13:06:01 -0800 Subject: [PATCH 018/307] docs: Add conceptual layout documentation for MainScreen This commit adds a new Markdown file, `Layout.md`, which provides a detailed conceptual overview of the `MainScreen` UI for both compact (phone) and expanded (tablet) layouts. The documentation includes ASCII diagrams to visually represent the component structure in each mode and highlights key architectural details. **Key areas covered:** * **Compact Layout (Phone/Portrait):** Explains the standard bottom navigation flow and the placement of the `DebugStackUi` within the bottom bar. * **Expanded Layout (Tablet/Landscape):** Describes the shift to a side navigation rail and the repositioning of the `DebugStackUi` to the top of the main content area. * **Logic Highlights:** Summarizes important dynamic behaviors, including automatic tab selection based on the navigation backstack, fold awareness to reset navigation, and the context-aware Floating Action Button (FAB). --- .../photodo/ui/navigation/Layout.md | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/Layout.md diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/Layout.md b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/Layout.md new file mode 100644 index 000000000..ced06b14a --- /dev/null +++ b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/Layout.md @@ -0,0 +1,71 @@ +Based on the code provided in `MainScreen.kt`, here is a conceptual preview of how your UI is laid out for both **Compact (Phone)** and **Expanded (Tablet/Desktop)** modes. + +### 1\. Compact Layout (Phone / Portrait) + +In this mode, the app uses a standard bottom navigation flow. Notably, your **Debug Stack UI** is attached to the bottom bar area. + +```text ++-------------------------------------------------+ +| [TopAppBar] | +| < Back Title (e.g., "PhotoDo") [Edit] | ++-------------------------------------------------+ +| | +| | +| | +| [Main Content Area] | +| (NavGraph: Home / List / Detail) | +| | +| | +| [FAB] | +| ( + ) | ++-------------------------------------------------+ +| [HomeBottomBar] (Home | Tasks | Settings) | ++-------------------------------------------------+ +| [DebugStackUi] (Black Bar: Stack/Category info)| <--- Placed below/inside BottomBar ++-------------------------------------------------+ +``` + +**Key Layout Details:** + +* **Navigation:** Controlled by `HomeBottomBar` at the bottom. +* **Debug UI:** The `DebugStackUi` is nested inside the `bottomBar` slot's `Column`. It will appear attached to the bottom of the navigation icons. +* **Content:** The `appContent` fills the space between the top bar and the bottom navigation assembly. + +----- + +### 2\. Expanded Layout (Tablet / Landscape) + +In this mode, the app shifts to a side navigation rail. The **Debug Stack UI** moves to the top of the content area. + +```text ++---+---------------------------------------------+ +| N | [TopAppBar] | +| A | Title (e.g., "Task Lists") [Trash] | +| V |---------------------------------------------| +| | [DebugStackUi] (Collapsible Debug Info) | <--- Placed at the top of content +| R |---------------------------------------------| +| A | | +| I | | +| L | | +| | [Main Content Area] | +| | (Adaptive List/Detail Strategy) | +| H | Pane 1: Categories | Pane 2: Tasks | +| O | | +| M | | +| E | [FAB] | +| | ( + ) | ++---+---------------------------------------------+ +``` + +**Key Layout Details:** + +* **Navigation:** `HomeNavigationRail` is fixed on the **Left**. +* **Debug UI:** The `DebugStackUi` is the first element in the content `Column`, placing it directly **under the Top App Bar**. +* **Content:** The `appContent` uses `Modifier.fillMaxSize()` to take up all remaining space below the debug bar. +* **Double Padding Fix:** Your recent fix ensures `appContent` doesn't have double padding, so it should sit flush against the Debug UI. + +### 3\. Logic Highlights + +* **Smart Tab Selection:** The tab selection (`currentTopLevelKey`) automatically updates based on the backstack state (`backStack.lastOrNull()`). If you are viewing a "Task List", the "Tasks" tab will highlight even if you navigated there from a different flow. +* **Fold Awareness:** The `LaunchedEffect(isExpandedScreen)` block detects if the device is folded (switching from Expanded to Compact). If this happens, it resets the navigation stack to the Root (Home) to prevent UI glitches where a Detail pane might get stuck without context. +* **Dynamic FAB:** The FAB (`FabMain`) changes its icon and action based on the current destination in the `NavGraph` (e.g., "Add Category" on Home vs "Add Item" on Detail), as passed up via `setFabState`. \ No newline at end of file From c5767f41d7ef8776a95a6bb72b1af93a384b4309 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 8 Dec 2025 13:11:33 -0800 Subject: [PATCH 019/307] docs: Add conceptual layout documentation for MainScreen This commit adds a new Markdown file, `Layout.md`, which provides a detailed conceptual overview of the `MainScreen` UI for both compact (phone) and expanded (tablet) layouts. The documentation includes ASCII diagrams to visually represent the component structure in each mode and highlights key architectural details. **Key areas covered:** * **Compact Layout (Phone/Portrait):** Explains the standard bottom navigation flow and the placement of the `DebugStackUi` within the bottom bar. * **Expanded Layout (Tablet/Landscape):** Describes the shift to a side navigation rail and the repositioning of the `DebugStackUi` to the top of the main content area. * **Logic Highlights:** Summarizes important dynamic behaviors, including automatic tab selection based on the navigation backstack, fold awareness to reset navigation, and the context-aware Floating Action Button (FAB). --- .../basepro/applications/photodo/ui/{navigation => }/Layout.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/{navigation => }/Layout.md (100%) diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/Layout.md b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/Layout.md similarity index 100% rename from applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/Layout.md rename to applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/Layout.md From 66355e0a73287eb934597b084a1495200f495687 Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 9 Dec 2025 12:29:07 -0800 Subject: [PATCH 020/307] fix(home): Ensure positive hue value for category card gradient This commit fixes a bug where the `hashCode()` of a category name could produce a negative integer, leading to an incorrect hue value and an `IllegalArgumentException` when creating the gradient color for the `CategoryCard`. By using `.absoluteValue` on the hash code, this change ensures that the resulting hue is always a positive number within the valid 0-360 range, preventing crashes and ensuring consistent color generation. --- .../photodo/features/home/ui/components/CategoryCard.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt index 6380ae966..2ec020955 100644 --- a/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt +++ b/applications/photodo/features/home/src/main/java/com/ylabz/basepro/applications/photodo/features/home/ui/components/CategoryCard.kt @@ -61,6 +61,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.ylabz.basepro.applications.photodo.features.home.ui.HomeEvent +import kotlin.math.absoluteValue @Composable fun CategoryCard( @@ -128,7 +129,7 @@ fun CategoryCard( // --- POP VISUALS END --- val coverGradient = remember(category.name) { - val hash = category.name.hashCode() + val hash = category.name.hashCode().absoluteValue val hue = (hash % 360).toFloat() val color1 = Color.hsv(hue, 0.6f, 0.8f) val color2 = Color.hsv((hue + 40) % 360, 0.5f, 0.9f) From 6beffe5497791729b1a10eea89e6cd1c141fd94b Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 9 Dec 2025 12:30:31 -0800 Subject: [PATCH 021/307] refactor(ui): Extract MainScreenContent to stateless composable This commit refactors the `MainScreen` composable by extracting its layout and rendering logic into a new, stateless composable named `MainScreenContent`. This improves the separation of concerns, making the UI easier to test, preview, and maintain. Previously, `MainScreen` was responsible for both state management and adaptive UI layout (switching between a `Row` with `NavigationRail` for expanded screens and a `Scaffold` with a `BottomBar` for compact screens). Now, `MainScreen` manages state and passes it down to `MainScreenContent`, which handles only the presentation logic. **Key Changes:** * **`MainScreen.kt`**: * The primary composable now focuses on collecting state, handling navigation logic, and orchestrating bottom sheets. * The complex `if (isExpandedScreen)` block for UI layout has been moved out. * It now calls the new `MainScreenContent` composable, passing all necessary state and callbacks as parameters. * Removed a significant amount of commented-out code and logging statements, cleaning up the function body. * **`MainScreenContent.kt` (New Composable)**: * A new, stateless composable that accepts all required data (`isExpandedScreen`, `uiState`, `currentTopLevelKey`, etc.) as parameters. * It contains the adaptive layout logic to render either the expanded (Row + Rail) or compact (Scaffold + BottomBar) view. * This extraction isolates UI presentation from business logic, making it more predictable and reusable. --- .../photodo/ui/navigation/main/MainScreen.kt | 361 ++++++------------ 1 file changed, 108 insertions(+), 253 deletions(-) diff --git a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt index 9c55cbb93..3c0a50d33 100644 --- a/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt +++ b/applications/photodo/src/main/java/com/ylabz/basepro/applications/photodo/ui/navigation/main/MainScreen.kt @@ -1,7 +1,5 @@ package com.ylabz.basepro.applications.photodo.ui.navigation.main -// --- THIS IS THE CORRECT, REAL IMPORT --- -// --- END --- import FabMain import android.annotation.SuppressLint import android.app.Activity @@ -74,17 +72,12 @@ fun MainScreen( val windowSizeClass = calculateWindowSizeClass(activity) val isExpandedScreen = windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact - // 1. COLLECT THE STATE from the single source of truth val uiState by mainScreenViewModel.uiState.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() - // --- Navigation State --- - // NOTE: **Bottom Navigation State** - // The actual navigation history for the NavDisplay. val backStack = rememberNavBackStack(PhotoDoNavKeys.HomeFeedKey) - // NOTE: **Adaptive Navigation State** val listDetailStrategy = rememberListDetailSceneStrategy() val currentListId by remember { @@ -94,50 +87,17 @@ fun MainScreen( } } - /* - val backStack = rememberNavBackStack(PhotoDoNavKeys.HomeFeedKey) - // 2. Use the *real* function from the 'adaptive-navigation3' library - val sceneStrategy = rememberListDetailSceneStrategy() - */ - - // --- Bottom Sheet State --- val modalSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val showBottomSheet = uiState.currentSheet != BottomSheetType.NONE - - // val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) - // --- Top Bar State --- + // --- Top Bar & FAB State --- var topBar: (@Composable (TopAppBarScrollBehavior) -> Unit) by remember { mutableStateOf({}) } - val setTopBar: (@Composable (TopAppBarScrollBehavior) -> Unit) -> Unit = { topBar = it } - // -- Fix this - //var topBar: @Composable () -> Unit by remember { mutableStateOf({}) } - //var fabState: FabStateMenu? by remember { mutableStateOf(null) } - // 1. Define the state for the FAB. It's nullable. var currentFabState by remember { mutableStateOf(null) } - // --- Back Stack Management --- - - //NOTE: **Top-Level Navigation State** - // (`currentTopLevelKey`): This tracks which main section of the app the user is in (e.g., "Home," "Tasks," or "Settings") - // The currently selected top-level tab. `rememberSaveable` ensures this state survives process death. - /* var currentTopLevelKey: NavKey by rememberSaveable(stateSaver = NavKeySaver) { - mutableStateOf(PhotoDoNavKeys.HomeFeedKey) - }*/ - - /*var currentTopLevelKey: NavKey by remember(backStack) { - mutableStateOf( - backStack.lastOrNull { entry: NavKey -> - TopLevelDestination.entries.any { dest -> dest.key::class == entry::class } - } ?: PhotoDoNavKeys.HomeFeedKey - ) - }*/ - // --- FIX: SMART TAB SELECTION --- - // 1. Check the TOP of the stack (Are we looking at a TaskList?) - // 2. If not, check the ROOT of the stack (Where did we start?) val currentTopLevelKey by remember { derivedStateOf { val top = backStack.lastOrNull() @@ -155,42 +115,18 @@ fun MainScreen( findTab(top) ?: findTab(root) ?: PhotoDoNavKeys.HomeFeedKey } } - // -------------------------------- - - // 1. Observe the categories from the HomeViewModel (you'll need to expose them or the HomeUiState) - // Ideally, MainScreenViewModel should hold this "global" state, but for now, let's assume - // you can check if the ID is valid. - - // A simpler fix for now: Make the default nullable and handle the null case. - // var lastSelectedCategoryId by rememberSaveable { mutableStateOf(null) } - - // Remember the last category ID the user interacted with. Default to 1L since we know it has data. - // --- Remembered State for "Add List" --- - // var lastSelectedCategoryId by rememberSaveable { mutableLongStateOf(1L) } // --- Navigation Handler --- - // ================================================================= - // START OF FIX - // ================================================================= val onNavigate: (NavKey) -> Unit = { navKey -> - // NAV_LOG: Log top-level tab navigation click Log.d(TAG, "NAVIGATION -- onNavigate triggered with navKey: ${navKey::class.simpleName}") - // When navigating via BottomBar/Rail, if the target is the List tab, - // use the last selected category ID instead of the hardcoded one. val keyToNavigate: NavKey = if (navKey is PhotoDoNavKeys.TaskListKey) { if (uiState.lastSelectedCategoryId != null) { - Log.d(TAG, "NAVIGATION -START- -> List tab clicked. Overriding to last selected categoryId: ${uiState.lastSelectedCategoryId}") - // Read from the uiState instead of the local variable - // CHECK: Do we have a valid category ID? PhotoDoNavKeys.TaskListKey(uiState.lastSelectedCategoryId!!) } else { - Log.d(TAG, "NAVIGATION -- -> List tab clicked, but no category is selected. Aborting navigation.") - // Abort navigation by returning null PhotoDoNavKeys.TaskListKey(null) } } else { - Log.d(TAG, "NAVIGATION -- -> Tab is not TaskListKey, using original key.") navKey } @@ -206,43 +142,12 @@ fun MainScreen( backStack.subList(1, backStack.size).clear() } } - - /*if (keyToNavigate != null) { - // --- THIS IS THE FIX --- - // Check if we are *already* at the root of the stack with this *exact* key. - // backStack.firstOrNull() checks the current root. - val isAlreadyAtRoot = backStack.firstOrNull() == keyToNavigate && backStack.size == 1 - - if (!isAlreadyAtRoot) { - // This block now correctly handles: - // 1. Switching to a new tab. - // 2. Popping to the root of the *current* tab (e.g., from a detail screen). - // 3. Switching between two keys of the *same* class (e.g., TaskListKey(2) -> TaskListKey(5)). - Log.d(TAG, "NAVIGATION -- -> Navigating to ${keyToNavigate::class.simpleName}. Replacing stack.") - currentTopLevelKey = keyToNavigate - backStack.replaceAll(keyToNavigate) // Clear history when switching tabs or popping to root - } else { - // We are already at the root of the correct tab, so do nothing. - Log.d(TAG, "NAVIGATION -- -> Already on top-level tab ${keyToNavigate::class.simpleName} at root. No change.") - } - // --- END OF FIX --- - - // NAV_LOG: Log navigation - Log.d(TAG, "NAVIGATION -DONE- onNavigate triggered with navKey: ${navKey::class.simpleName}") - } else { - Log.d(TAG, "NAVIGATION -- Aborted.") - } */ } - // ================================================================= - // END OF FIX - // ================================================================= // A key that forces recomposition when the back stack changes. - // CORRECTED KEY: This now uses derivedStateOf to be state-aware. val backStackKey by remember { derivedStateOf { backStack.joinToString("-") { navKey -> - Log.d(TAG, "NAVIGATION -RECOMPOSITION- onNavigate triggered with navKey: ${navKey::class.simpleName}") when (navKey) { is PhotoDoNavKeys.TaskListKey -> "TaskList(${navKey.categoryId})" else -> navKey.javaClass.simpleName @@ -252,111 +157,135 @@ fun MainScreen( } // --- FIX: RESET STACK ON FOLD For Testing --- - // When the device is folded (Expanded -> Compact), we force the stack - // back to the root. This ensures the user sees the "List" (Categories) - // instead of getting stuck on the "Detail" (Task List). - // --- FIX: RESET STACK ON FOLD --- LaunchedEffect(isExpandedScreen) { if (!isExpandedScreen) { - // LOGGING: See exactly what is in the stack when you fold Log.d(TAG, "Fold Detected. Current Stack Size: ${backStack.size}") - backStack.forEachIndexed { index, key -> - Log.d(TAG, "Stack [$index]: ${key::class.simpleName}") - } - if (backStack.size > 1) { Log.d(TAG, "Resetting stack to Root (Home).") - // Correctly keep the first item (Home), remove the rest (Details) backStack.subList(1, backStack.size).clear() - } else { - Log.d(TAG, "Stack size is 1. Already at Root (or Navigation was incorrect).") } } } - // NAV_LOG: Log the current back stack state before rendering AppContent Log.d(TAG, "BackStack state before AppContent: $backStackKey") - // --- 4. Define the app content ONCE --- - // This is the NavGraph that contains all the screens - val appContent = @Composable { modifier: Modifier -> - key(backStackKey) { - if (backStack.isNotEmpty()) { - PhotoDoNavGraph( - modifier = modifier, - backStack = backStack, - sceneStrategy = listDetailStrategy, - isExpandedScreen = isExpandedScreen, - scrollBehavior = scrollBehavior, - setTopBar = { topBar = { it(scrollBehavior) } }, - // THIS IS WHERE THE FUNCTION IS CREATED AND PASSED DOWN - setFabState = { newFabState -> currentFabState = newFabState }, - // setFabState = { fabState = it }, - onCategorySelected = { categoryId -> - // NAV_LOG: Log when the last selected category ID is updated - Log.d(TAG, "onCategorySelected callback triggered. Updating lastSelectedCategoryId to: $categoryId") - mainScreenViewModel.onEvent(MainScreenEvent.OnCategorySelected(categoryId)) - }, - // Pass the ACTIONS down. - onEvent = mainScreenViewModel::onEvent + // --- RENDER CONTENT --- + // We pass the "NavGraph" as a lambda to the stateless content + MainScreenContent( + isExpandedScreen = isExpandedScreen, + uiState = uiState, + currentTopLevelKey = currentTopLevelKey, + currentFabState = currentFabState, + backStackKey = backStackKey, + currentListId = currentListId, + scrollBehavior = scrollBehavior, + topBar = topBar, + onNavigate = onNavigate, + onEvent = mainScreenViewModel::onEvent, + content = { modifier -> + // The actual Navigation Graph + key(backStackKey) { + if (backStack.isNotEmpty()) { + PhotoDoNavGraph( + modifier = modifier, + backStack = backStack, + sceneStrategy = listDetailStrategy, + isExpandedScreen = isExpandedScreen, + scrollBehavior = scrollBehavior, + setTopBar = { topBar = { it(scrollBehavior) } }, + setFabState = { newFabState -> currentFabState = newFabState }, + onCategorySelected = { categoryId -> + Log.d(TAG, "onCategorySelected callback triggered. Updating lastSelectedCategoryId to: $categoryId") + mainScreenViewModel.onEvent(MainScreenEvent.OnCategorySelected(categoryId)) + }, + onEvent = mainScreenViewModel::onEvent + ) + } + } + } + ) + // --- BOTTOM SHEETS --- + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + mainScreenViewModel.onEvent(MainScreenEvent.OnBottomSheetDismissed) + }, + sheetState = modalSheetState + ) { + when (uiState.currentSheet) { + BottomSheetType.ADD_CATEGORY -> { + AddCategoryBottomSheet( + uiState = uiState, + onEvent = mainScreenViewModel::onEvent + ) + } + BottomSheetType.ADD_LIST -> { + AddListSheet( + state = uiState, + onEvent = mainScreenViewModel::onEvent + ) + } + BottomSheetType.ADD_ITEM -> AddItemBottomSheet( + onAddClick = { + Log.d(TAG, "Adding Item (Photo) - Not yet implemented") + scope.launch { modalSheetState.hide() }.invokeOnCompletion { + mainScreenViewModel.onEvent(MainScreenEvent.OnBottomSheetDismissed) + } + } ) + BottomSheetType.NONE -> {} } } } +} - /** - *Explanation of the Comments: - * Main Block Comment: This provides a high-level overview of what the entire if/else statement is for, explaining that it's the central piece of the adaptive UI. - * Expanded Layout Comment: This comment specifically describes the "if" part of the logic, explaining why a Row and HomeNavigationRail are used for larger screens. - * Compact Layout Comment: This describes the "else" block, clarifying that a standard Scaffold with a HomeBottomBar is the appropriate choice for smaller, vertical screens like phones. - * These comments should make the code much easier for other developers (and your future self!) to understand at a glance. - */ - - /** - * --------------------------------------------------------------------------------- - * ADAPTIVE UI SCAFFOLDING - * --------------------------------------------------------------------------------- - * This logic block is the core of the app's adaptive layout. It checks the - * 'isExpandedScreen' boolean to determine the device's screen size category - * (calculated using Material 3's WindowSizeClass). - * - * - For EXPANDED screens (like tablets or desktops), it creates a side-by-side - * layout using a Row, placing a NavigationRail on the left and the main - * app content on the right. - * - * - For COMPACT screens (like most phones in portrait mode), it uses a standard - * Scaffold with a HomeBottomBar at the bottom for navigation. - * - * This approach ensures the UI is optimized for different form factors, providing - * an intuitive user experience on any device. - * --------------------------------------------------------------------------------- - */ +/** + * --------------------------------------------------------------------------------- + * STATELESS COMPOSABLE: MainScreenContent + * --------------------------------------------------------------------------------- + * This extracted composable handles the ADAPTIVE UI SCAFFOLDING layout. + * It separates the layout logic (Row vs Column) from the ViewModel and State logic. + * + * This allows for: + * 1. Previews (by passing mock state). + * 2. Easier testing. + * 3. Fixing layout bugs (like double padding) in one place. + * --------------------------------------------------------------------------------- + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreenContent( + isExpandedScreen: Boolean, + uiState: MainScreenUiState, // Ensure this matches your actual State class name + currentTopLevelKey: NavKey, + currentFabState: FabState?, + backStackKey: String, + currentListId: String?, + scrollBehavior: TopAppBarScrollBehavior, + topBar: @Composable (TopAppBarScrollBehavior) -> Unit, + onNavigate: (NavKey) -> Unit, + onEvent: (MainScreenEvent) -> Unit, + content: @Composable (Modifier) -> Unit +) { if (isExpandedScreen) { - // **Expanded Layout: Show Navigation Rail** + // **Expanded Layout: Show Navigation Rail** Row(modifier = Modifier.fillMaxSize()) { HomeNavigationRail( currentTopLevelKey = currentTopLevelKey, onNavigate = onNavigate - /* onNavigate = { key -> - if (key::class != currentTopLevelKey::class) { - currentTopLevelKey = key - backStack.replace(key) - } - }*/ ) Scaffold( - // **Expanded Layout: Show Bottom Bar** modifier = Modifier .weight(1f) .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { topBar(scrollBehavior) }, floatingActionButton = { - // 7. Render the FAB from the VM state FabMain(fabState = currentFabState) } ) { padding -> Column( + // 1. This padding handles the TopBar offset correctly modifier = Modifier.padding(padding) ) { DebugStackUi( @@ -365,13 +294,17 @@ fun MainScreen( currentListId = currentListId, currentFabState = currentFabState ) - // appContent(Modifier.padding(padding)) - appContent(Modifier.fillMaxSize()) + + // --- FIX: NO PADDING HERE --- + // The parent Column is already padded. passing Modifier.fillMaxSize() + // ensures the content fills the remaining space without adding + // a second layer of padding. + content(Modifier.fillMaxSize()) } } } } else { -// **Compact Layout: Show Bottom Bar** + // **Compact Layout: Show Bottom Bar** Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { topBar(scrollBehavior) }, @@ -380,12 +313,6 @@ fun MainScreen( HomeBottomBar( currentTopLevelKey = currentTopLevelKey, onNavigate = onNavigate - /*onNavigate = { key -> - if (key::class != currentTopLevelKey::class) { - currentTopLevelKey = key - backStack.replaceTop(key) - } - }*/ ) // Add the new collapsible debug UI DebugStackUi( @@ -397,97 +324,25 @@ fun MainScreen( } }, floatingActionButton = { - // 7. Render the FAB from the VM state FabMain(fabState = currentFabState) } ) { padding -> - appContent(Modifier.padding(padding)) - } - } -// --- END OF YOUR CORRECT LAYOUT LOGIC --- - -// --- END OF YOUR CORRECT LAYOUT LOGIC --- -// G. SHOW BOTTOM SHEETS based on the collected state - if (showBottomSheet) { - ModalBottomSheet( - onDismissRequest = { - mainScreenViewModel.onEvent(MainScreenEvent.OnBottomSheetDismissed) - }, - sheetState = modalSheetState - ) { - when (uiState.currentSheet) { - // --- UPDATED: CLEAN CALL SITE --- - BottomSheetType.ADD_CATEGORY -> { - AddCategoryBottomSheet( - uiState = uiState, - onEvent = mainScreenViewModel::onEvent - ) - } - // -------------------------------- - - // --- UPDATED CALL SITE --- - BottomSheetType.ADD_LIST -> { - AddListSheet( - state = uiState, - onEvent = mainScreenViewModel::onEvent - ) - } - // ------------------------- - - /*BottomSheetType.ADD_LIST -> AddListBottomSheet( - onDismiss = { mainScreenViewModel.onEvent(MainScreenEvent.OnBottomSheetDismissed) }, - onSaveList = { title: String, description: String -> - // --- NEW LOGIC --- - // Get the current category ID from the ViewModel's state - val currentCategoryId = uiState.lastSelectedCategoryId - - // 1. Send the save event with all necessary data - if (currentCategoryId != null) { - // YES: Unwrap it (smart cast to Long) and send the event - mainScreenViewModel.onEvent( - MainScreenEvent.OnSaveList( - title = title, - description = description, - categoryId = currentCategoryId - ) - ) - } else { - // NO: Do not send the event. Show an error or log it. - Log.e("MainScreen", "Cannot save list. No category selected.") - - // 2. Dismiss the sheet (or use the onDismiss handler) - mainScreenViewModel.onEvent(MainScreenEvent.OnBottomSheetDismissed) - // --- END NEW LOGIC --- - } - } - )*/ - - BottomSheetType.ADD_ITEM -> AddItemBottomSheet( - onAddClick = { - Log.d(TAG, "Adding Item (Photo) - Not yet implemented") - scope.launch { modalSheetState.hide() }.invokeOnCompletion { - mainScreenViewModel.onEvent(MainScreenEvent.OnBottomSheetDismissed) - } - } - ) - - BottomSheetType.NONE -> {} - } + // In compact mode, we DO need to pass the padding down, because + // the content is a direct child of Scaffold, not wrapped in a padded Column. + content(Modifier.padding(padding)) } } } - - @Composable fun AddItemBottomSheet(onAddClick: () -> Unit) { Text("Add Item Bottom Sheet") } -// Helper extension functions +// Helper extension functions fun MutableList.replaceAll(item: T) { clear(); add(item) } fun MutableList.replaceTop(item: T) { if (isNotEmpty()) this[lastIndex] = item else add(item) -} +} \ No newline at end of file From 7e0f5f1b5756a19946397646d38e89723cef264d Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 9 Dec 2025 13:27:10 -0800 Subject: [PATCH 022/307] feat(wear): Add initial Wear OS module for Ash-Bike This commit introduces a new Wear OS application module, `ashbike:apps:wear`, to the `ashbike` project. This foundational module sets up a basic Wear OS app with a main activity, a tile, and a complication. The new app is standalone and built with Jetpack Compose for Wear OS, following modern Android development practices. **Key Changes:** * **New Wear OS Module (`:applications:ashbike:apps:wear`)**: * Added a complete Gradle setup (`build.gradle.kts`) with dependencies for Wear Compose, Tiles, Complications, and Horologist. * Configured the `AndroidManifest.xml` for a standalone watch app, registering the main activity, a tile service, and a complication service. * Included standard project files like `.gitignore`, `proguard-rules.pro`, and `lint.xml`. * **Main Application (`MainActivity.kt`)**: * A simple `ComponentActivity` using `setContent` to display a "Hello World" Composable. * Implements a splash screen using `Theme.SplashScreen`. * Includes a basic `BaseProTheme` for future styling. * **Wear Tile (`MainTileService.kt`)**: * A `SuspendingTileService` (from Horologist) is implemented to provide a basic tile that displays "Hello World!". * Includes preview setup for different Wear device shapes. * **Wear Complication (`MainComplicationService.kt`)**: * A `SuspendingComplicationDataSourceService` is set up to provide a `SHORT_TEXT` complication that displays the abbreviated current day of the week (e.g., "Mon"). * **Resources and Assets**: * Added new launcher icons (`ic_launcher`), including adaptive and round versions. * Added a drawable for the splash screen icon. * Included necessary string resources for the app, tile, and complication labels. --- applications/ashbike/apps/wear/.gitignore | 1 + .../ashbike/apps/wear/build.gradle.kts | 66 +++++++ applications/ashbike/apps/wear/lint.xml | 8 + .../ashbike/apps/wear/proguard-rules.pro | 21 +++ .../apps/wear/src/main/AndroidManifest.xml | 71 ++++++++ .../complication/MainComplicationService.kt | 41 +++++ .../ashbike/wear/presentation/MainActivity.kt | 72 ++++++++ .../ashbike/wear/presentation/theme/Theme.kt | 17 ++ .../ashbike/wear/tile/MainTileService.kt | 85 +++++++++ .../main/res/drawable-round/tile_preview.png | Bin 0 -> 14977 bytes .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++++ .../src/main/res/drawable/splash_icon.xml | 27 +++ .../src/main/res/drawable/tile_preview.png | Bin 0 -> 8505 bytes .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../src/main/res/values-round/strings.xml | 3 + .../apps/wear/src/main/res/values/strings.xml | 10 ++ .../apps/wear/src/main/res/values/styles.xml | 8 + gradle/libs.versions.toml | 2 + 30 files changed, 644 insertions(+) create mode 100644 applications/ashbike/apps/wear/.gitignore create mode 100644 applications/ashbike/apps/wear/build.gradle.kts create mode 100644 applications/ashbike/apps/wear/lint.xml create mode 100644 applications/ashbike/apps/wear/proguard-rules.pro create mode 100644 applications/ashbike/apps/wear/src/main/AndroidManifest.xml create mode 100644 applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/complication/MainComplicationService.kt create mode 100644 applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/presentation/MainActivity.kt create mode 100644 applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/presentation/theme/Theme.kt create mode 100644 applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/tile/MainTileService.kt create mode 100644 applications/ashbike/apps/wear/src/main/res/drawable-round/tile_preview.png create mode 100644 applications/ashbike/apps/wear/src/main/res/drawable/ic_launcher_background.xml create mode 100644 applications/ashbike/apps/wear/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 applications/ashbike/apps/wear/src/main/res/drawable/splash_icon.xml create mode 100644 applications/ashbike/apps/wear/src/main/res/drawable/tile_preview.png create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/wear/src/main/res/values-round/strings.xml create mode 100644 applications/ashbike/apps/wear/src/main/res/values/strings.xml create mode 100644 applications/ashbike/apps/wear/src/main/res/values/styles.xml diff --git a/applications/ashbike/apps/wear/.gitignore b/applications/ashbike/apps/wear/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/applications/ashbike/apps/wear/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/applications/ashbike/apps/wear/build.gradle.kts b/applications/ashbike/apps/wear/build.gradle.kts new file mode 100644 index 000000000..b6f1a03ac --- /dev/null +++ b/applications/ashbike/apps/wear/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.ylabz.basepro.ashbike.wear" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "com.ylabz.basepro.ashbike.wear" + minSdk = 35 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + useLibrary("wear-sdk") + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.google.play.services.wearable) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.wear.compose.material) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.wear.tooling.preview) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.tiles) + implementation(libs.androidx.tiles.material) + implementation(libs.androidx.tiles.tooling.preview) + implementation(libs.horologist.compose.tools) + implementation(libs.horologist.tiles) + implementation(libs.androidx.watchface.complications.data.source.ktx) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + debugImplementation(libs.androidx.tiles.tooling) +} \ No newline at end of file diff --git a/applications/ashbike/apps/wear/lint.xml b/applications/ashbike/apps/wear/lint.xml new file mode 100644 index 000000000..44fac75b8 --- /dev/null +++ b/applications/ashbike/apps/wear/lint.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/applications/ashbike/apps/wear/proguard-rules.pro b/applications/ashbike/apps/wear/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/applications/ashbike/apps/wear/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/AndroidManifest.xml b/applications/ashbike/apps/wear/src/main/AndroidManifest.xml new file mode 100644 index 000000000..360a3f8b1 --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/AndroidManifest.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/complication/MainComplicationService.kt b/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/complication/MainComplicationService.kt new file mode 100644 index 000000000..9aef93c9f --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/complication/MainComplicationService.kt @@ -0,0 +1,41 @@ +package com.ylabz.basepro.ashbike.wear.complication + +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.ShortTextComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService +import java.util.Calendar + +/** + * Skeleton for complication data source that returns short text. + */ +class MainComplicationService : SuspendingComplicationDataSourceService() { + + override fun getPreviewData(type: ComplicationType): ComplicationData? { + if (type != ComplicationType.SHORT_TEXT) { + return null + } + return createComplicationData("Mon", "Monday") + } + + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData { + return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { + Calendar.SUNDAY -> createComplicationData("Sun", "Sunday") + Calendar.MONDAY -> createComplicationData("Mon", "Monday") + Calendar.TUESDAY -> createComplicationData("Tue", "Tuesday") + Calendar.WEDNESDAY -> createComplicationData("Wed", "Wednesday") + Calendar.THURSDAY -> createComplicationData("Thu", "Thursday") + Calendar.FRIDAY -> createComplicationData("Fri!", "Friday!") + Calendar.SATURDAY -> createComplicationData("Sat", "Saturday") + else -> throw IllegalArgumentException("too many days") + } + } + + private fun createComplicationData(text: String, contentDescription: String) = + ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder(text).build(), + contentDescription = PlainComplicationText.Builder(contentDescription).build() + ).build() +} \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/presentation/MainActivity.kt b/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/presentation/MainActivity.kt new file mode 100644 index 000000000..0b59b3a7c --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/presentation/MainActivity.kt @@ -0,0 +1,72 @@ +/* While this template provides a good starting point for using Wear Compose, you can always + * take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter to find the + * most up to date changes to the libraries and their usages. + */ + +package com.ylabz.basepro.ashbike.wear.presentation + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.Text +import androidx.wear.compose.material.TimeText +import androidx.wear.tooling.preview.devices.WearDevices +import com.ylabz.basepro.ashbike.wear.R +import com.ylabz.basepro.ashbike.wear.presentation.theme.BaseProTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + + super.onCreate(savedInstanceState) + + setTheme(android.R.style.Theme_DeviceDefault) + + setContent { + WearApp("Android") + } + } +} + +@Composable +fun WearApp(greetingName: String) { + BaseProTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colors.background), + contentAlignment = Alignment.Center + ) { + TimeText() + Greeting(greetingName = greetingName) + } + } +} + +@Composable +fun Greeting(greetingName: String) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary, + text = stringResource(R.string.hello_world, greetingName) + ) +} + +@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true) +@Composable +fun DefaultPreview() { + WearApp("Preview Android") +} \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/presentation/theme/Theme.kt b/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/presentation/theme/Theme.kt new file mode 100644 index 000000000..91a8e9ad7 --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/presentation/theme/Theme.kt @@ -0,0 +1,17 @@ +package com.ylabz.basepro.ashbike.wear.presentation.theme + +import androidx.compose.runtime.Composable +import androidx.wear.compose.material.MaterialTheme + +@Composable +fun BaseProTheme( + content: @Composable () -> Unit +) { + /** + * Empty theme to customize for your app. + * See: https://developer.android.com/jetpack/compose/designsystems/custom + */ + MaterialTheme( + content = content + ) +} \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/tile/MainTileService.kt b/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/tile/MainTileService.kt new file mode 100644 index 000000000..43a8d2d01 --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/java/com/ylabz/basepro/ashbike/wear/tile/MainTileService.kt @@ -0,0 +1,85 @@ +package com.ylabz.basepro.ashbike.wear.tile + +import android.content.Context +import androidx.wear.protolayout.ColorBuilders.argb +import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.ResourceBuilders +import androidx.wear.protolayout.TimelineBuilders +import androidx.wear.protolayout.material.Colors +import androidx.wear.protolayout.material.Text +import androidx.wear.protolayout.material.Typography +import androidx.wear.protolayout.material.layouts.PrimaryLayout +import androidx.wear.tiles.RequestBuilders +import androidx.wear.tiles.TileBuilders +import androidx.wear.tiles.tooling.preview.Preview +import androidx.wear.tiles.tooling.preview.TilePreviewData +import androidx.wear.tooling.preview.devices.WearDevices +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.tiles.SuspendingTileService + +private const val RESOURCES_VERSION = "0" + +/** + * Skeleton for a tile with no images. + */ +@OptIn(ExperimentalHorologistApi::class) +class MainTileService : SuspendingTileService() { + + override suspend fun resourcesRequest( + requestParams: RequestBuilders.ResourcesRequest + ) = resources(requestParams) + + override suspend fun tileRequest( + requestParams: RequestBuilders.TileRequest + ) = tile(requestParams, this) +} + +private fun resources( + requestParams: RequestBuilders.ResourcesRequest +): ResourceBuilders.Resources { + return ResourceBuilders.Resources.Builder() + .setVersion(RESOURCES_VERSION) + .build() +} + +private fun tile( + requestParams: RequestBuilders.TileRequest, + context: Context, +): TileBuilders.Tile { + val singleTileTimeline = TimelineBuilders.Timeline.Builder() + .addTimelineEntry( + TimelineBuilders.TimelineEntry.Builder() + .setLayout( + LayoutElementBuilders.Layout.Builder() + .setRoot(tileLayout(requestParams, context)) + .build() + ) + .build() + ) + .build() + + return TileBuilders.Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline(singleTileTimeline) + .build() +} + +private fun tileLayout( + requestParams: RequestBuilders.TileRequest, + context: Context, +): LayoutElementBuilders.LayoutElement { + return PrimaryLayout.Builder(requestParams.deviceConfiguration) + .setResponsiveContentInsetEnabled(true) + .setContent( + Text.Builder(context, "Hello World!") + .setColor(argb(Colors.DEFAULT.onSurface)) + .setTypography(Typography.TYPOGRAPHY_CAPTION1) + .build() + ).build() +} + +@Preview(device = WearDevices.SMALL_ROUND) +@Preview(device = WearDevices.LARGE_ROUND) +fun tilePreview(context: Context) = TilePreviewData(::resources) { + tile(it, context) +} \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/res/drawable-round/tile_preview.png b/applications/ashbike/apps/wear/src/main/res/drawable-round/tile_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..474fac4de00792a8752a07bde5ab511987f4d1df GIT binary patch literal 14977 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 INSOxzf2& + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/applications/ashbike/apps/wear/src/main/res/drawable/ic_launcher_foreground.xml b/applications/ashbike/apps/wear/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/res/drawable/splash_icon.xml b/applications/ashbike/apps/wear/src/main/res/drawable/splash_icon.xml new file mode 100644 index 000000000..7874e83f0 --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/applications/ashbike/apps/wear/src/main/res/drawable/tile_preview.png b/applications/ashbike/apps/wear/src/main/res/drawable/tile_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..8cbe3ed5a91a4a0f2955494ee1d0ce003e83cfa7 GIT binary patch literal 8505 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 + + + + + \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/applications/ashbike/apps/wear/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/res/mipmap-hdpi/ic_launcher.webp b/applications/ashbike/apps/wear/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/applications/ashbike/apps/wear/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/applications/ashbike/apps/wear/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/applications/ashbike/apps/wear/src/main/res/mipmap-mdpi/ic_launcher.webp b/applications/ashbike/apps/wear/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/applications/ashbike/apps/wear/src/main/res/mipmap-xhdpi/ic_launcher.webp b/applications/ashbike/apps/wear/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/applications/ashbike/apps/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/applications/ashbike/apps/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/applications/ashbike/apps/wear/src/main/res/values-round/strings.xml b/applications/ashbike/apps/wear/src/main/res/values-round/strings.xml new file mode 100644 index 000000000..42f12297f --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/res/values-round/strings.xml @@ -0,0 +1,3 @@ + + From the Round world,\nHello, %1$s! + \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/res/values/strings.xml b/applications/ashbike/apps/wear/src/main/res/values/strings.xml new file mode 100644 index 000000000..597519167 --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + Wear + + From the Square world,\nHello, %1$s! + Example tile + Example complication + \ No newline at end of file diff --git a/applications/ashbike/apps/wear/src/main/res/values/styles.xml b/applications/ashbike/apps/wear/src/main/res/values/styles.xml new file mode 100644 index 000000000..dbf9c83a4 --- /dev/null +++ b/applications/ashbike/apps/wear/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2a9580e1..2e9b729a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,6 +112,7 @@ espressoCore = "3.7.0" # AndroidX Espresso, for Android UI testing uiautomator = "2.3.0" # AndroidX UI Automator, for UI testing across apps androidxProfiling = "1.4.1" # Version for AndroidX Benchmark, Baseline Profile, ProfileInstaller runner = "1.7.0" +composeFoundation = "1.2.1" @@ -283,6 +284,7 @@ androidx-runner = { group = "androidx.test", name = "runner", version.ref = "run # Testing - Benchmarking & Profiling androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidxProfiling" } androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfiling" } +androidx-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "composeFoundation" } [plugins] From 53fa0bc75fcefe4346f1826c9380a2571bf8ff4b Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 9 Dec 2025 13:32:52 -0800 Subject: [PATCH 023/307] feat(ashbike): Scaffold mobile and wear app modules This commit introduces the initial scaffolding for the `ashbike` application, creating two new Gradle modules: one for the mobile app and one for the Wear OS app. The mobile app module (`applications/ashbike/apps/mobile`) is set up as a standard Android application using Jetpack Compose. **Key Changes:** * **`settings.gradle.kts`**: * Added `:applications:ashbike:apps:mobile` and `:applications/ashbike:apps:wear` to include the new modules in the build. * **`ashbike/apps/mobile` Module:** * **Gradle Setup**: Configured `build.gradle.kts` with the Android application plugin, Compose, and standard dependencies for UI, testing, and lifecycle management. * **Android Manifest**: A basic `AndroidManifest.xml` is created, defining the `MainActivity` as the launcher activity. * **Source Code**: Includes a boilerplate `MainActivity.kt` with a "Hello Android" `Greeting` composable, along with default `ui.theme` files. * **Resources**: Added standard app resources, including launcher icons, string/color/theme definitions, and Proguard rules. * **Tests**: Initial unit and instrumented test files have been added. --- .idea/gradle.xml | 3 + applications/ashbike/apps/mobile/.gitignore | 1 + .../ashbike/apps/mobile/build.gradle.kts | 60 +++++++ .../ashbike/apps/mobile/proguard-rules.pro | 21 +++ .../ashbike/mobile/ExampleInstrumentedTest.kt | 24 +++ .../apps/mobile/src/main/AndroidManifest.xml | 24 +++ .../basepro/ashbike/mobile/MainActivity.kt | 47 +++++ .../basepro/ashbike/mobile/ui/theme/Color.kt | 11 ++ .../basepro/ashbike/mobile/ui/theme/Theme.kt | 58 ++++++ .../basepro/ashbike/mobile/ui/theme/Type.kt | 34 ++++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++++ .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../mobile/src/main/res/values/colors.xml | 10 ++ .../mobile/src/main/res/values/strings.xml | 3 + .../mobile/src/main/res/values/themes.xml | 5 + .../basepro/ashbike/mobile/ExampleUnitTest.kt | 17 ++ settings.gradle.kts | 2 + 29 files changed, 532 insertions(+) create mode 100644 applications/ashbike/apps/mobile/.gitignore create mode 100644 applications/ashbike/apps/mobile/build.gradle.kts create mode 100644 applications/ashbike/apps/mobile/proguard-rules.pro create mode 100644 applications/ashbike/apps/mobile/src/androidTest/java/com/ylabz/basepro/ashbike/mobile/ExampleInstrumentedTest.kt create mode 100644 applications/ashbike/apps/mobile/src/main/AndroidManifest.xml create mode 100644 applications/ashbike/apps/mobile/src/main/java/com/ylabz/basepro/ashbike/mobile/MainActivity.kt create mode 100644 applications/ashbike/apps/mobile/src/main/java/com/ylabz/basepro/ashbike/mobile/ui/theme/Color.kt create mode 100644 applications/ashbike/apps/mobile/src/main/java/com/ylabz/basepro/ashbike/mobile/ui/theme/Theme.kt create mode 100644 applications/ashbike/apps/mobile/src/main/java/com/ylabz/basepro/ashbike/mobile/ui/theme/Type.kt create mode 100644 applications/ashbike/apps/mobile/src/main/res/drawable/ic_launcher_background.xml create mode 100644 applications/ashbike/apps/mobile/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 applications/ashbike/apps/mobile/src/main/res/values/colors.xml create mode 100644 applications/ashbike/apps/mobile/src/main/res/values/strings.xml create mode 100644 applications/ashbike/apps/mobile/src/main/res/values/themes.xml create mode 100644 applications/ashbike/apps/mobile/src/test/java/com/ylabz/basepro/ashbike/mobile/ExampleUnitTest.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ca05549a4..17aea720d 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -13,6 +13,9 @@

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/applications/ashbike/apps/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp b/applications/ashbike/apps/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/applications/ashbike/apps/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/applications/ashbike/apps/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/applications/ashbike/apps/mobile/src/main/res/values/colors.xml b/applications/ashbike/apps/mobile/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/applications/ashbike/apps/mobile/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/applications/ashbike/apps/mobile/src/main/res/values/strings.xml b/applications/ashbike/apps/mobile/src/main/res/values/strings.xml new file mode 100644 index 000000000..122aff1c9 --- /dev/null +++ b/applications/ashbike/apps/mobile/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Mobile + \ No newline at end of file diff --git a/applications/ashbike/apps/mobile/src/main/res/values/themes.xml b/applications/ashbike/apps/mobile/src/main/res/values/themes.xml new file mode 100644 index 000000000..e86cfa81f --- /dev/null +++ b/applications/ashbike/apps/mobile/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +