Skip to content

Commit 9493111

Browse files
tjohnson009frettclaude
authored
GT-2990 Localization Settings Box (Tools Layout) (#4422)
* Add Surface box to end of tools layout for personalization * Create and insert localizations settings box for tools and lessons pages; lessons WIP * Add string resources for lessons layout personalization * Add UiEvent for going to localization settings from lesson layout * Add locationSettingsBox to lessons layout; Make padding similar to tools layout * Remove unused imports * Remove unnecessary Row composable for center alignment; Remove unused imports and style declarations * Remove unused imports * Gate the localization settings banner based on the isPersonalizationEnabled flag * Minor lint fix * Make localizationSettingsBox internal following lessons and tools layout convention * Revert erroneous padding changes and correct personalization if statement * Correct personalization if statement for personalization settings box * Add snapshot tests for lessons and tools localization settings box * Record updated snapshots * Retrigger CI * Update modifier line to single line Co-authored-by: Daniel Frett <frett@users.noreply.github.com> * Add a custom vertical arrangement to LazyColumn to pin localization to the bottom * Record updated snapshots * Retrigger CI * Bring in rememberPinLastArrangement composable to both tools and lessons * Create rememberPinLastItemBottomArrangement composable for reuse in tools and lessons * Adjust the rememberlastItem call to reflect changes * Remove unnecessary key from remember and params * Rename and generalize PinLastItemBottomArrangement to FloatLastItemsToBottomArrangement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update LocalizationSettingsBox typography to titleMedium and bodyMedium Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: tjohnson009 <tjohnson009@users.noreply.github.com> Co-authored-by: Daniel Frett <frett@users.noreply.github.com> Co-authored-by: Daniel Frett <daniel.frett@cru.org> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent eee004b commit 9493111

21 files changed

Lines changed: 277 additions & 11 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.cru.godtools.ui.dashboard
2+
3+
import androidx.annotation.StringRes
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.material3.Button
8+
import androidx.compose.material3.MaterialTheme
9+
import androidx.compose.material3.Surface
10+
import androidx.compose.material3.Text
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Alignment
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.res.stringResource
15+
import androidx.compose.ui.text.font.FontWeight
16+
import androidx.compose.ui.unit.dp
17+
import org.cru.godtools.R
18+
19+
@Composable
20+
internal fun LocalizationSettingsBox(
21+
@StringRes title: Int,
22+
@StringRes description: Int,
23+
onClickSettings: () -> Unit,
24+
modifier: Modifier = Modifier,
25+
) {
26+
Surface(
27+
color = MaterialTheme.colorScheme.primaryContainer,
28+
modifier = modifier.fillMaxWidth(),
29+
) {
30+
Column(modifier = Modifier.padding(16.dp)) {
31+
Text(
32+
text = stringResource(title),
33+
style = MaterialTheme.typography.titleMedium
34+
)
35+
Text(
36+
text = stringResource(description),
37+
style = MaterialTheme.typography.bodyMedium
38+
)
39+
Button(
40+
onClick = onClickSettings,
41+
modifier = Modifier
42+
.align(Alignment.CenterHorizontally)
43+
.padding(top = 8.dp)
44+
) {
45+
Text(stringResource(R.string.dashboard_section_localization_box_button))
46+
}
47+
}
48+
}
49+
}

app/src/main/kotlin/org/cru/godtools/ui/dashboard/lessons/LessonsLayout.kt

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.cru.godtools.ui.dashboard.lessons
22

33
import androidx.compose.foundation.layout.Column
4-
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.foundation.layout.Spacer
5+
import androidx.compose.foundation.layout.fillMaxHeight
56
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.height
68
import androidx.compose.foundation.layout.padding
79
import androidx.compose.foundation.layout.wrapContentWidth
810
import androidx.compose.foundation.lazy.LazyColumn
@@ -20,16 +22,26 @@ import androidx.compose.ui.res.stringResource
2022
import androidx.compose.ui.unit.dp
2123
import com.slack.circuit.codegen.annotations.CircuitInject
2224
import dagger.hilt.components.SingletonComponent
25+
import org.ccci.gto.android.common.compose.foundation.layout.padding
2326
import org.cru.godtools.R
2427
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen
28+
import org.cru.godtools.ui.dashboard.LocalizationSettingsBox
2529
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiEvent
2630
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiState
31+
import org.cru.godtools.ui.dashboard.personalization.rememberFloatLastItemsToBottomArrangement
2732
import org.cru.godtools.ui.tools.LessonToolCard
2833

34+
internal val MARGIN_LESSONS_LAYOUT_HORIZONTAL = 16.dp
35+
2936
@Composable
3037
@CircuitInject(LessonsScreen::class, SingletonComponent::class)
3138
internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
32-
LazyColumn(contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp), modifier = modifier) {
39+
LazyColumn(
40+
verticalArrangement = rememberFloatLastItemsToBottomArrangement(
41+
numToFloat = if (state.mode == UiState.Mode.PERSONALIZATION) 1 else 0
42+
),
43+
modifier = modifier.fillMaxHeight()
44+
) {
3345
if (state.isPersonalizationEnabled) {
3446
item("mode-toggle", "mode-toggle") {
3547
PersonalizationToggle(
@@ -42,9 +54,11 @@ internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
4254
}
4355

4456
item("header", "header") {
45-
LessonsHeader(state.mode, modifier = Modifier.padding(top = 16.dp))
46-
HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp))
47-
LessonFilters(state)
57+
LessonsHeader(state.mode, Modifier.padding(top = 16.dp, horizontal = MARGIN_LESSONS_LAYOUT_HORIZONTAL))
58+
HorizontalDivider(
59+
modifier = Modifier.padding(vertical = 12.dp, horizontal = MARGIN_LESSONS_LAYOUT_HORIZONTAL)
60+
)
61+
LessonFilters(state, modifier = Modifier.padding(horizontal = MARGIN_LESSONS_LAYOUT_HORIZONTAL))
4862
}
4963

5064
items(state.lessons, { it.toolCode.orEmpty() }, { "lesson" }) { toolState ->
@@ -54,9 +68,23 @@ internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
5468
showProgress = true,
5569
modifier = Modifier
5670
.animateItem()
57-
.padding(top = 16.dp)
71+
.padding(top = 16.dp, horizontal = MARGIN_LESSONS_LAYOUT_HORIZONTAL)
5872
)
5973
}
74+
75+
item("spacer", "spacer") {
76+
Spacer(modifier = Modifier.height(16.dp))
77+
}
78+
79+
if (state.mode == UiState.Mode.PERSONALIZATION) {
80+
item("localization-settings-box", "localization-settings-box") {
81+
LocalizationSettingsBox(
82+
title = R.string.dashboard_lessons_section_personalized_localization_title,
83+
description = R.string.dashboard_lessons_section_personalized_localization_text,
84+
onClickSettings = { state.eventSink(UiEvent.OpenLocalizationSettings) }
85+
)
86+
}
87+
}
6088
}
6189
}
6290

app/src/main/kotlin/org/cru/godtools/ui/dashboard/lessons/LessonsPresenter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import org.cru.godtools.sync.GodToolsSyncService
5454
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
5555
import org.cru.godtools.ui.dashboard.filters.FilterMenu
5656
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiState
57+
import org.cru.godtools.ui.settings.country.CountrySettingsScreen
5758
import org.cru.godtools.ui.tools.ToolCardPresenter
5859
import org.cru.godtools.ui.tools.ToolCardPresenter.ToolCardEvent
5960
import org.cru.godtools.util.createToolIntent
@@ -89,6 +90,7 @@ class LessonsPresenter @AssistedInject internal constructor(
8990

9091
internal sealed interface UiEvent : CircuitUiEvent {
9192
data class ChangeMode(val mode: UiState.Mode) : UiEvent
93+
data object OpenLocalizationSettings : UiEvent
9294
}
9395
// endregion UiState / UiEvent
9496

@@ -114,6 +116,7 @@ class LessonsPresenter @AssistedInject internal constructor(
114116
) {
115117
when (it) {
116118
is UiEvent.ChangeMode -> mode = it.mode
119+
is UiEvent.OpenLocalizationSettings -> navigator.goTo(CountrySettingsScreen)
117120
}
118121
}
119122
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.cru.godtools.ui.dashboard.personalization
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.ui.unit.Density
7+
8+
@Composable
9+
internal fun rememberFloatLastItemsToBottomArrangement(numToFloat: Int) =
10+
remember(numToFloat) { FloatLastItemsToBottomArrangement(numToFloat) }
11+
12+
internal class FloatLastItemsToBottomArrangement(val numToFloat: Int) : Arrangement.Vertical {
13+
override fun Density.arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
14+
var currentOffset = 0
15+
sizes.forEachIndexed { index, size ->
16+
outPositions[index] = currentOffset
17+
currentOffset += size
18+
}
19+
20+
if (currentOffset < totalSize && numToFloat > 0) {
21+
currentOffset = totalSize
22+
sizes.takeLast(numToFloat).reversed().forEachIndexed { index, size ->
23+
currentOffset -= size
24+
outPositions[sizes.lastIndex - index] = currentOffset
25+
}
26+
}
27+
}
28+
}

app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayout.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.cru.godtools.ui.dashboard.tools
33
import androidx.compose.foundation.layout.Arrangement
44
import androidx.compose.foundation.layout.Column
55
import androidx.compose.foundation.layout.PaddingValues
6+
import androidx.compose.foundation.layout.fillMaxHeight
67
import androidx.compose.foundation.layout.fillMaxWidth
78
import androidx.compose.foundation.layout.padding
89
import androidx.compose.foundation.layout.wrapContentWidth
@@ -30,6 +31,8 @@ import org.ccci.gto.android.common.compose.foundation.layout.padding
3031
import org.cru.godtools.R
3132
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen
3233
import org.cru.godtools.ui.banner.Banners
34+
import org.cru.godtools.ui.dashboard.LocalizationSettingsBox
35+
import org.cru.godtools.ui.dashboard.personalization.rememberFloatLastItemsToBottomArrangement
3336
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiEvent
3437
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState
3538
import org.cru.godtools.ui.tools.SquareToolCard
@@ -47,7 +50,13 @@ internal fun ToolsLayout(state: UiState, modifier: Modifier = Modifier) {
4750
val columnState = rememberLazyListState()
4851
LaunchedEffect(state.banner?.type) { if (state.banner != null) columnState.animateScrollToItem(0) }
4952

50-
LazyColumn(state = columnState, modifier = modifier) {
53+
LazyColumn(
54+
state = columnState,
55+
verticalArrangement = rememberFloatLastItemsToBottomArrangement(
56+
numToFloat = if (state.mode == UiState.Mode.PERSONALIZATION) 1 else 0
57+
),
58+
modifier = modifier.fillMaxHeight()
59+
) {
5160
if (!state.dataLoaded) return@LazyColumn
5261

5362
item("banners", "banners") {
@@ -121,6 +130,16 @@ internal fun ToolsLayout(state: UiState, modifier: Modifier = Modifier) {
121130
.padding(bottom = 16.dp, horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL)
122131
)
123132
}
133+
134+
if (state.mode == UiState.Mode.PERSONALIZATION) {
135+
item("localization-settings-box", "localization-settings-box") {
136+
LocalizationSettingsBox(
137+
title = R.string.dashboard_tools_section_personalized_localization_title,
138+
description = R.string.dashboard_tools_section_personalized_localization_text,
139+
onClickSettings = { state.eventSink(UiEvent.OpenLocalizationSettings) }
140+
)
141+
}
142+
}
124143
}
125144
}
126145

app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
4343
import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters
4444
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState
4545
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode
46+
import org.cru.godtools.ui.settings.country.CountrySettingsScreen
4647
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen
4748
import org.cru.godtools.ui.tools.ToolCardPresenter
4849
import org.cru.godtools.ui.tools.ToolCardPresenter.ToolCardEvent
@@ -79,6 +80,7 @@ class ToolsPresenter @AssistedInject internal constructor(
7980

8081
sealed interface UiEvent : CircuitUiEvent {
8182
data class ChangeMode(val mode: Mode) : UiEvent
83+
data object OpenLocalizationSettings : UiEvent
8284
}
8385
// endregion UiState / UiEvent
8486

@@ -120,6 +122,7 @@ class ToolsPresenter @AssistedInject internal constructor(
120122
) {
121123
when (it) {
122124
is UiEvent.ChangeMode -> mode = it.mode
125+
is UiEvent.OpenLocalizationSettings -> navigator.goTo(CountrySettingsScreen)
123126
}
124127
}
125128
}

app/src/main/res/values/strings_dashboard.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ An online version can be found at https://knowgod.com/</string>
4949
</plurals>
5050
<string name="dashboard_lessons_progress_completed">Completed</string>
5151
<string name="dashboard_lessons_progress_in_progress">%1$d%% Complete</string>
52+
<string name="dashboard_lessons_section_personalized_localization_title">Displaying localized Lessons list</string>
53+
<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>
5254

5355
<!-- Home -->
5456
<eat-comment />
@@ -83,6 +85,9 @@ An online version can be found at https://knowgod.com/</string>
8385
<string name="dashboard_tools_section_categories_all">All Tools</string>
8486
<string name="dashboard_tools_section_spotlight_label">Featured</string>
8587
<string name="dashboard_tools_section_spotlight_description">Here are some tools we thought you might like</string>
88+
<string name="dashboard_tools_section_personalized_localization_title">Displaying localized Tools list</string>
89+
<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>
90+
<string name="dashboard_section_localization_box_button">Change Localization Settings</string>
8691

8792
<!-- Tool Cards -->
8893
<eat-comment />
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.cru.godtools.ui.dashboard.personalization
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.ui.unit.Density
5+
import kotlin.test.Test
6+
import kotlin.test.assertContentEquals
7+
8+
class FloatLastItemsToBottomArrangementTest {
9+
private val density = Density(1f)
10+
11+
private fun Arrangement.Vertical.testArrange(totalSize: Int, vararg sizes: Int): IntArray {
12+
val outPositions = IntArray(sizes.size)
13+
with(density) { arrange(totalSize, sizes, outPositions) }
14+
return outPositions
15+
}
16+
17+
// region numToFloat = 0
18+
@Test
19+
fun `numToFloat=0 - items positioned sequentially from top`() {
20+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 0)
21+
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(500, 100, 100, 100))
22+
}
23+
// endregion numToFloat = 0
24+
25+
// region numToFloat = 1
26+
@Test
27+
fun `numToFloat=1 - last item floats to bottom when content fits`() {
28+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
29+
assertContentEquals(intArrayOf(0, 100, 400), arrangement.testArrange(500, 100, 100, 100))
30+
}
31+
32+
@Test
33+
fun `numToFloat=1 - no floating when content overflows container`() {
34+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
35+
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(200, 100, 100, 100))
36+
}
37+
38+
@Test
39+
fun `numToFloat=1 - single item floats to bottom`() {
40+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
41+
assertContentEquals(intArrayOf(400), arrangement.testArrange(500, 100))
42+
}
43+
// endregion numToFloat = 1
44+
45+
// region numToFloat = 2
46+
@Test
47+
fun `numToFloat=2 - last 2 items float to bottom when content fits`() {
48+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 2)
49+
assertContentEquals(intArrayOf(0, 300, 400), arrangement.testArrange(500, 100, 100, 100))
50+
}
51+
52+
@Test
53+
fun `numToFloat=2 - no floating when content exactly fills container`() {
54+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 2)
55+
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(300, 100, 100, 100))
56+
}
57+
// endregion numToFloat = 2
58+
59+
// region numToFloat exceeds item count
60+
@Test
61+
fun `numToFloat exceeds item count - all items float to bottom`() {
62+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 5)
63+
assertContentEquals(intArrayOf(200, 300, 400), arrangement.testArrange(500, 100, 100, 100))
64+
}
65+
// endregion numToFloat exceeds item count
66+
67+
// region edge cases
68+
@Test
69+
fun `empty item list`() {
70+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
71+
assertContentEquals(intArrayOf(), arrangement.testArrange(500))
72+
}
73+
74+
@Test
75+
fun `variable item sizes - floating item positioned correctly`() {
76+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
77+
assertContentEquals(intArrayOf(0, 50, 350), arrangement.testArrange(500, 50, 200, 150))
78+
}
79+
// endregion edge cases
80+
}
Loading
Loading

0 commit comments

Comments
 (0)