Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cc91d14
Add Surface box to end of tools layout for personalization
tjohnson009 May 8, 2026
cf69aa8
Create and insert localizations settings box for tools and lessons pa…
tjohnson009 May 8, 2026
fc99ef8
Add string resources for lessons layout personalization
tjohnson009 May 8, 2026
42a0af0
Add UiEvent for going to localization settings from lesson layout
tjohnson009 May 8, 2026
5ccbf15
Add locationSettingsBox to lessons layout; Make padding similar to to…
tjohnson009 May 8, 2026
87a4eb8
Remove unused imports
tjohnson009 May 8, 2026
963815e
Remove unnecessary Row composable for center alignment; Remove unused…
tjohnson009 May 8, 2026
b3e1396
Remove unused imports
tjohnson009 May 8, 2026
1a2bc22
Gate the localization settings banner based on the isPersonalizationE…
tjohnson009 May 8, 2026
5112561
Minor lint fix
tjohnson009 May 8, 2026
210a55d
Make localizationSettingsBox internal following lessons and tools lay…
tjohnson009 May 8, 2026
63f45a8
Revert erroneous padding changes and correct personalization if state…
tjohnson009 May 12, 2026
91e8186
Correct personalization if statement for personalization settings box
tjohnson009 May 12, 2026
91983dd
Add snapshot tests for lessons and tools localization settings box
tjohnson009 May 12, 2026
8603cfb
Record updated snapshots
tjohnson009 May 12, 2026
a02e9fd
Retrigger CI
tjohnson009 May 12, 2026
eddeda0
Update modifier line to single line
tjohnson009 May 12, 2026
88f340b
Add a custom vertical arrangement to LazyColumn to pin localization t…
tjohnson009 May 15, 2026
d4f475e
Record updated snapshots
tjohnson009 May 15, 2026
24a5326
Retrigger CI
tjohnson009 May 12, 2026
9cdeb78
Bring in rememberPinLastArrangement composable to both tools and lessons
tjohnson009 May 15, 2026
f4c0a1b
Create rememberPinLastItemBottomArrangement composable for reuse in t…
tjohnson009 May 15, 2026
b5647d1
Adjust the rememberlastItem call to reflect changes
tjohnson009 May 22, 2026
6cb5543
Remove unnecessary key from remember and params
tjohnson009 May 22, 2026
d8e3e97
Rename and generalize PinLastItemBottomArrangement to FloatLastItemsT…
frett May 26, 2026
089ff26
Update LocalizationSettingsBox typography to titleMedium and bodyMedium
frett May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.cru.godtools.ui.dashboard

import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.font.FontWeight
import androidx.compose.ui.unit.dp
import org.cru.godtools.R

@Composable
internal fun LocalizationSettingsBox(
@StringRes title: Int,
@StringRes description: Int,
onClickSettings: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
modifier = modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleMedium
)
Text(
text = stringResource(description),
style = MaterialTheme.typography.bodyMedium
)
Button(
onClick = onClickSettings,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 8.dp)
) {
Text(stringResource(R.string.dashboard_section_localization_box_button))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.cru.godtools.ui.dashboard.lessons

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
Expand All @@ -20,16 +22,26 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.slack.circuit.codegen.annotations.CircuitInject
import dagger.hilt.components.SingletonComponent
import org.ccci.gto.android.common.compose.foundation.layout.padding
import org.cru.godtools.R
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen
import org.cru.godtools.ui.dashboard.LocalizationSettingsBox
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiEvent
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiState
import org.cru.godtools.ui.dashboard.personalization.rememberFloatLastItemsToBottomArrangement
import org.cru.godtools.ui.tools.LessonToolCard

internal val MARGIN_LESSONS_LAYOUT_HORIZONTAL = 16.dp

@Composable
@CircuitInject(LessonsScreen::class, SingletonComponent::class)
internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
LazyColumn(contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp), modifier = modifier) {
Comment thread
tjohnson009 marked this conversation as resolved.
LazyColumn(
verticalArrangement = rememberFloatLastItemsToBottomArrangement(
numToFloat = if (state.mode == UiState.Mode.PERSONALIZATION) 1 else 0
),
modifier = modifier.fillMaxHeight()
) {
if (state.isPersonalizationEnabled) {
item("mode-toggle", "mode-toggle") {
PersonalizationToggle(
Expand All @@ -42,9 +54,11 @@ internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
}

item("header", "header") {
LessonsHeader(state.mode, modifier = Modifier.padding(top = 16.dp))
HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp))
LessonFilters(state)
LessonsHeader(state.mode, Modifier.padding(top = 16.dp, horizontal = MARGIN_LESSONS_LAYOUT_HORIZONTAL))
HorizontalDivider(
modifier = Modifier.padding(vertical = 12.dp, horizontal = MARGIN_LESSONS_LAYOUT_HORIZONTAL)
)
LessonFilters(state, modifier = Modifier.padding(horizontal = MARGIN_LESSONS_LAYOUT_HORIZONTAL))
}

items(state.lessons, { it.toolCode.orEmpty() }, { "lesson" }) { toolState ->
Expand All @@ -54,9 +68,23 @@ internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
showProgress = true,
modifier = Modifier
.animateItem()
.padding(top = 16.dp)
.padding(top = 16.dp, horizontal = MARGIN_LESSONS_LAYOUT_HORIZONTAL)
)
}

item("spacer", "spacer") {
Spacer(modifier = Modifier.height(16.dp))
}

if (state.mode == UiState.Mode.PERSONALIZATION) {
item("localization-settings-box", "localization-settings-box") {
LocalizationSettingsBox(
title = R.string.dashboard_lessons_section_personalized_localization_title,
description = R.string.dashboard_lessons_section_personalized_localization_text,
onClickSettings = { state.eventSink(UiEvent.OpenLocalizationSettings) }
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import org.cru.godtools.sync.GodToolsSyncService
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
import org.cru.godtools.ui.dashboard.filters.FilterMenu
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiState
import org.cru.godtools.ui.settings.country.CountrySettingsScreen
import org.cru.godtools.ui.tools.ToolCardPresenter
import org.cru.godtools.ui.tools.ToolCardPresenter.ToolCardEvent
import org.cru.godtools.util.createToolIntent
Expand Down Expand Up @@ -89,6 +90,7 @@ class LessonsPresenter @AssistedInject internal constructor(

internal sealed interface UiEvent : CircuitUiEvent {
data class ChangeMode(val mode: UiState.Mode) : UiEvent
data object OpenLocalizationSettings : UiEvent
}
// endregion UiState / UiEvent

Expand All @@ -114,6 +116,7 @@ class LessonsPresenter @AssistedInject internal constructor(
) {
when (it) {
is UiEvent.ChangeMode -> mode = it.mode
is UiEvent.OpenLocalizationSettings -> navigator.goTo(CountrySettingsScreen)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.cru.godtools.ui.dashboard.personalization

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.Density

@Composable
internal fun rememberFloatLastItemsToBottomArrangement(numToFloat: Int) =
remember(numToFloat) { FloatLastItemsToBottomArrangement(numToFloat) }

internal class FloatLastItemsToBottomArrangement(val numToFloat: Int) : Arrangement.Vertical {
override fun Density.arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
var currentOffset = 0
sizes.forEachIndexed { index, size ->
outPositions[index] = currentOffset
currentOffset += size
}

if (currentOffset < totalSize && numToFloat > 0) {
currentOffset = totalSize
sizes.takeLast(numToFloat).reversed().forEachIndexed { index, size ->
currentOffset -= size
outPositions[sizes.lastIndex - index] = currentOffset
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.cru.godtools.ui.dashboard.tools
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
Expand Down Expand Up @@ -30,6 +31,8 @@ import org.ccci.gto.android.common.compose.foundation.layout.padding
import org.cru.godtools.R
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen
import org.cru.godtools.ui.banner.Banners
import org.cru.godtools.ui.dashboard.LocalizationSettingsBox
import org.cru.godtools.ui.dashboard.personalization.rememberFloatLastItemsToBottomArrangement
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiEvent
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState
import org.cru.godtools.ui.tools.SquareToolCard
Expand All @@ -47,7 +50,13 @@ internal fun ToolsLayout(state: UiState, modifier: Modifier = Modifier) {
val columnState = rememberLazyListState()
LaunchedEffect(state.banner?.type) { if (state.banner != null) columnState.animateScrollToItem(0) }

LazyColumn(state = columnState, modifier = modifier) {
LazyColumn(
state = columnState,
verticalArrangement = rememberFloatLastItemsToBottomArrangement(
numToFloat = if (state.mode == UiState.Mode.PERSONALIZATION) 1 else 0
),
modifier = modifier.fillMaxHeight()
) {
if (!state.dataLoaded) return@LazyColumn

item("banners", "banners") {
Expand Down Expand Up @@ -121,6 +130,16 @@ internal fun ToolsLayout(state: UiState, modifier: Modifier = Modifier) {
.padding(bottom = 16.dp, horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL)
)
}

if (state.mode == UiState.Mode.PERSONALIZATION) {
item("localization-settings-box", "localization-settings-box") {
LocalizationSettingsBox(
title = R.string.dashboard_tools_section_personalized_localization_title,
description = R.string.dashboard_tools_section_personalized_localization_text,
onClickSettings = { state.eventSink(UiEvent.OpenLocalizationSettings) }
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode
import org.cru.godtools.ui.settings.country.CountrySettingsScreen
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen
import org.cru.godtools.ui.tools.ToolCardPresenter
import org.cru.godtools.ui.tools.ToolCardPresenter.ToolCardEvent
Expand Down Expand Up @@ -79,6 +80,7 @@ class ToolsPresenter @AssistedInject internal constructor(

sealed interface UiEvent : CircuitUiEvent {
data class ChangeMode(val mode: Mode) : UiEvent
data object OpenLocalizationSettings : UiEvent
}
// endregion UiState / UiEvent

Expand Down Expand Up @@ -120,6 +122,7 @@ class ToolsPresenter @AssistedInject internal constructor(
) {
when (it) {
is UiEvent.ChangeMode -> mode = it.mode
is UiEvent.OpenLocalizationSettings -> navigator.goTo(CountrySettingsScreen)
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings_dashboard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ An online version can be found at https://knowgod.com/</string>
</plurals>
<string name="dashboard_lessons_progress_completed">Completed</string>
<string name="dashboard_lessons_progress_in_progress">%1$d%% Complete</string>
<string name="dashboard_lessons_section_personalized_localization_title">Displaying localized Lessons list</string>
<string name="dashboard_lessons_section_personalized_localization_text">The lessons shown on this page are based on your Localization setting. You can alter this at any time.</string>

<!-- Home -->
<eat-comment />
Expand Down Expand Up @@ -83,6 +85,9 @@ An online version can be found at https://knowgod.com/</string>
<string name="dashboard_tools_section_categories_all">All Tools</string>
<string name="dashboard_tools_section_spotlight_label">Featured</string>
<string name="dashboard_tools_section_spotlight_description">Here are some tools we thought you might like</string>
<string name="dashboard_tools_section_personalized_localization_title">Displaying localized Tools list</string>
<string name="dashboard_tools_section_personalized_localization_text">The tools shown in your Personalized Tools page are selected based on your Language and Localization setting. You can alter this by editing your setting.</string>
<string name="dashboard_section_localization_box_button">Change Localization Settings</string>

<!-- Tool Cards -->
<eat-comment />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.cru.godtools.ui.dashboard.personalization

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.unit.Density
import kotlin.test.Test
import kotlin.test.assertContentEquals

class FloatLastItemsToBottomArrangementTest {
private val density = Density(1f)

private fun Arrangement.Vertical.testArrange(totalSize: Int, vararg sizes: Int): IntArray {
val outPositions = IntArray(sizes.size)
with(density) { arrange(totalSize, sizes, outPositions) }
return outPositions
}

// region numToFloat = 0
@Test
fun `numToFloat=0 - items positioned sequentially from top`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 0)
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(500, 100, 100, 100))
}
// endregion numToFloat = 0

// region numToFloat = 1
@Test
fun `numToFloat=1 - last item floats to bottom when content fits`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
assertContentEquals(intArrayOf(0, 100, 400), arrangement.testArrange(500, 100, 100, 100))
}

@Test
fun `numToFloat=1 - no floating when content overflows container`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(200, 100, 100, 100))
}

@Test
fun `numToFloat=1 - single item floats to bottom`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
assertContentEquals(intArrayOf(400), arrangement.testArrange(500, 100))
}
// endregion numToFloat = 1

// region numToFloat = 2
@Test
fun `numToFloat=2 - last 2 items float to bottom when content fits`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 2)
assertContentEquals(intArrayOf(0, 300, 400), arrangement.testArrange(500, 100, 100, 100))
}

@Test
fun `numToFloat=2 - no floating when content exactly fills container`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 2)
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(300, 100, 100, 100))
}
// endregion numToFloat = 2

// region numToFloat exceeds item count
@Test
fun `numToFloat exceeds item count - all items float to bottom`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 5)
assertContentEquals(intArrayOf(200, 300, 400), arrangement.testArrange(500, 100, 100, 100))
}
// endregion numToFloat exceeds item count

// region edge cases
@Test
fun `empty item list`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
assertContentEquals(intArrayOf(), arrangement.testArrange(500))
}

@Test
fun `variable item sizes - floating item positioned correctly`() {
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
assertContentEquals(intArrayOf(0, 50, 350), arrangement.testArrange(500, 50, 200, 150))
}
// endregion edge cases
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading