Skip to content

Commit 7b123b9

Browse files
authored
Merge pull request #815 from ooni/scrollbars
Add scrollbars on desktop for main screens
2 parents 93f0ae3 + c8325f1 commit 7b123b9

File tree

14 files changed

+298
-143
lines changed

14 files changed

+298
-143
lines changed

composeApp/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ kotlin {
170170
compilerOptions {
171171
// Common compiler options applied to all Kotlin source sets
172172
freeCompilerArgs.add("-Xexpect-actual-classes")
173+
// Switch to future default rule: https://youtrack.jetbrains.com/issue/KT-73255
174+
freeCompilerArgs.add("-Xannotation-default-target=param-property")
173175
}
174176
}
175177

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.ooni.probe.ui.shared
2+
3+
import androidx.compose.foundation.ScrollState
4+
import androidx.compose.foundation.lazy.LazyListState
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.Modifier
7+
8+
@Composable
9+
actual fun VerticalScrollbar(
10+
state: LazyListState,
11+
modifier: Modifier,
12+
) {
13+
}
14+
15+
@Composable
16+
actual fun VerticalScrollbar(
17+
state: ScrollState,
18+
modifier: Modifier,
19+
) {
20+
}

composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/InstalledTestDescriptorModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ private fun dateTimeFormat(monthNames: List<String>) =
108108
Format {
109109
monthName(MonthNames(monthNames))
110110
char(' ')
111-
dayOfMonth()
111+
day()
112112
chars(", ")
113113
year()
114114
}

composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
1414
import androidx.compose.foundation.layout.statusBars
1515
import androidx.compose.foundation.lazy.LazyColumn
1616
import androidx.compose.foundation.lazy.items
17+
import androidx.compose.foundation.lazy.rememberLazyListState
1718
import androidx.compose.foundation.shape.RoundedCornerShape
1819
import androidx.compose.material3.Icon
1920
import androidx.compose.material3.MaterialTheme
@@ -43,6 +44,7 @@ import org.ooni.probe.data.models.DescriptorUpdateOperationState
4344
import org.ooni.probe.ui.shared.IgnoreBatteryOptimizationDialog
4445
import org.ooni.probe.ui.shared.TestRunErrorMessages
4546
import org.ooni.probe.ui.shared.UpdateProgressStatus
47+
import org.ooni.probe.ui.shared.VerticalScrollbar
4648
import org.ooni.probe.ui.shared.isHeightCompact
4749
import org.ooni.probe.ui.theme.AppTheme
4850

@@ -107,30 +109,38 @@ fun DashboardScreen(
107109
VpnWarning()
108110
}
109111

110-
LazyColumn(
111-
modifier = Modifier.padding(top = if (isHeightCompact()) 8.dp else 24.dp)
112-
.testTag("Dashboard-List"),
113-
contentPadding = PaddingValues(bottom = 16.dp),
114-
) {
115-
val allSectionsHaveValues = state.descriptors.entries.all { it.value.any() }
116-
state.descriptors.forEach { (type, items) ->
117-
if (allSectionsHaveValues && items.isNotEmpty()) {
118-
item(type) {
119-
TestDescriptorSection(type)
112+
Box {
113+
val lazyListState = rememberLazyListState()
114+
LazyColumn(
115+
modifier = Modifier.padding(top = if (isHeightCompact()) 8.dp else 24.dp)
116+
.testTag("Dashboard-List"),
117+
contentPadding = PaddingValues(bottom = 16.dp),
118+
state = lazyListState,
119+
) {
120+
val allSectionsHaveValues = state.descriptors.entries.all { it.value.any() }
121+
state.descriptors.forEach { (type, items) ->
122+
if (allSectionsHaveValues && items.isNotEmpty()) {
123+
item(type) {
124+
TestDescriptorSection(type)
125+
}
126+
}
127+
items(items, key = { it.key }) { descriptor ->
128+
TestDescriptorItem(
129+
descriptor = descriptor,
130+
onClick = {
131+
onEvent(DashboardViewModel.Event.DescriptorClicked(descriptor))
132+
},
133+
onUpdateClick = {
134+
onEvent(DashboardViewModel.Event.UpdateDescriptorClicked(descriptor))
135+
},
136+
)
120137
}
121-
}
122-
items(items, key = { it.key }) { descriptor ->
123-
TestDescriptorItem(
124-
descriptor = descriptor,
125-
onClick = {
126-
onEvent(DashboardViewModel.Event.DescriptorClicked(descriptor))
127-
},
128-
onUpdateClick = {
129-
onEvent(DashboardViewModel.Event.UpdateDescriptorClicked(descriptor))
130-
},
131-
)
132138
}
133139
}
140+
VerticalScrollbar(
141+
state = lazyListState,
142+
modifier = Modifier.align(Alignment.CenterEnd),
143+
)
134144
}
135145
}
136146

composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.size
1818
import androidx.compose.foundation.layout.wrapContentHeight
1919
import androidx.compose.foundation.lazy.LazyColumn
2020
import androidx.compose.foundation.lazy.items
21+
import androidx.compose.foundation.lazy.rememberLazyListState
2122
import androidx.compose.foundation.pager.HorizontalPager
2223
import androidx.compose.foundation.pager.rememberPagerState
2324
import androidx.compose.foundation.shape.CircleShape
@@ -92,6 +93,7 @@ import org.ooni.probe.ui.result.ResultViewModel.MeasurementGroupItem.Group
9293
import org.ooni.probe.ui.result.ResultViewModel.MeasurementGroupItem.Single
9394
import org.ooni.probe.ui.results.UploadResults
9495
import org.ooni.probe.ui.shared.TopBar
96+
import org.ooni.probe.ui.shared.VerticalScrollbar
9597
import org.ooni.probe.ui.shared.formatDataUsage
9698
import org.ooni.probe.ui.shared.isHeightCompact
9799
import org.ooni.probe.ui.shared.longFormat
@@ -148,53 +150,61 @@ fun ResultScreen(
148150
if (state.result == null) return@Column
149151
val showSummary = !isHeightCompact()
150152

151-
LazyColumn(
152-
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
153-
) {
154-
if (showSummary) {
155-
item("summary") {
156-
Surface(
157-
color = descriptorColor,
158-
contentColor = onDescriptorColor,
159-
) {
160-
Summary(state.result)
153+
Box {
154+
val lazyListState = rememberLazyListState()
155+
LazyColumn(
156+
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
157+
state = lazyListState,
158+
) {
159+
if (showSummary) {
160+
item("summary") {
161+
Surface(
162+
color = descriptorColor,
163+
contentColor = onDescriptorColor,
164+
) {
165+
Summary(state.result)
166+
}
161167
}
162168
}
163-
}
164169

165-
if (state.result.anyMeasurementMissingUpload) {
166-
stickyHeader("upload_results") {
167-
UploadResults(onUploadClick = { onEvent(ResultViewModel.Event.UploadClicked) })
170+
if (state.result.anyMeasurementMissingUpload) {
171+
stickyHeader("upload_results") {
172+
UploadResults(onUploadClick = { onEvent(ResultViewModel.Event.UploadClicked) })
173+
}
168174
}
169-
}
170175

171-
items(state.groupedMeasurements, key = { item ->
172-
when (item) {
173-
is Group -> item.test.name
174-
is Single -> item.measurement.measurement.idOrThrow.value
175-
}
176-
}) { item ->
177-
when (item) {
178-
is Group -> {
179-
ResultGroupMeasurementCell(
180-
item = item,
181-
isResultDone = state.result.result.isDone,
182-
onClick = { onEvent(ResultViewModel.Event.MeasurementClicked(it)) },
183-
onDropdownToggled = {
184-
onEvent(ResultViewModel.Event.MeasurementGroupToggled(item))
185-
},
186-
)
176+
items(state.groupedMeasurements, key = { item ->
177+
when (item) {
178+
is Group -> item.test.name
179+
is Single -> item.measurement.measurement.idOrThrow.value
187180
}
188-
189-
is Single -> {
190-
ResultMeasurementCell(
191-
item = item.measurement,
192-
isResultDone = state.result.result.isDone,
193-
onClick = { onEvent(ResultViewModel.Event.MeasurementClicked(it)) },
194-
)
181+
}) { item ->
182+
when (item) {
183+
is Group -> {
184+
ResultGroupMeasurementCell(
185+
item = item,
186+
isResultDone = state.result.result.isDone,
187+
onClick = { onEvent(ResultViewModel.Event.MeasurementClicked(it)) },
188+
onDropdownToggled = {
189+
onEvent(ResultViewModel.Event.MeasurementGroupToggled(item))
190+
},
191+
)
192+
}
193+
194+
is Single -> {
195+
ResultMeasurementCell(
196+
item = item.measurement,
197+
isResultDone = state.result.result.isDone,
198+
onClick = { onEvent(ResultViewModel.Event.MeasurementClicked(it)) },
199+
)
200+
}
195201
}
196202
}
197203
}
204+
VerticalScrollbar(
205+
state = lazyListState,
206+
modifier = Modifier.align(Alignment.CenterEnd),
207+
)
198208
}
199209
}
200210

composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.padding
1313
import androidx.compose.foundation.layout.size
1414
import androidx.compose.foundation.lazy.LazyColumn
1515
import androidx.compose.foundation.lazy.items
16+
import androidx.compose.foundation.lazy.rememberLazyListState
1617
import androidx.compose.foundation.selection.triStateToggleable
1718
import androidx.compose.material.icons.Icons
1819
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -89,6 +90,7 @@ import org.ooni.probe.data.models.ResultFilter
8990
import org.ooni.probe.shared.stringMonthArrayResource
9091
import org.ooni.probe.ui.shared.LightStatusBars
9192
import org.ooni.probe.ui.shared.TopBar
93+
import org.ooni.probe.ui.shared.VerticalScrollbar
9294
import org.ooni.probe.ui.shared.formatDataUsage
9395
import org.ooni.probe.ui.shared.isHeightCompact
9496

@@ -244,64 +246,71 @@ fun ResultsScreen(
244246
UploadResults(onUploadClick = { onEvent(ResultsViewModel.Event.UploadClick) })
245247
}
246248

247-
LazyColumn {
248-
state.results.forEach { (date, results) ->
249-
stickyHeader(key = date.toString()) {
250-
ResultDateHeader(date)
251-
}
252-
items(items = results) { result ->
253-
val isSelected = result.isSelected
254-
ResultCell(
255-
item = result.item,
256-
onResultClick = {
257-
if (state.selectionEnabled) {
258-
onEvent(
259-
ResultsViewModel.Event.ToggleItemSelection(
260-
result.item,
261-
!isSelected,
262-
),
263-
)
264-
} else {
265-
onEvent(ResultsViewModel.Event.ResultClick(result.item))
266-
}
267-
},
268-
isSelected = isSelected,
269-
onSelectChange = { checked ->
270-
onEvent(
271-
ResultsViewModel.Event.ToggleItemSelection(
272-
result.item,
273-
checked,
274-
),
275-
)
276-
},
277-
onLongClick = {
278-
if (!isSelected) {
249+
Box {
250+
val lazyListState = rememberLazyListState()
251+
LazyColumn(state = lazyListState) {
252+
state.results.forEach { (date, results) ->
253+
stickyHeader(key = date.toString()) {
254+
ResultDateHeader(date)
255+
}
256+
items(items = results) { result ->
257+
val isSelected = result.isSelected
258+
ResultCell(
259+
item = result.item,
260+
onResultClick = {
261+
if (state.selectionEnabled) {
262+
onEvent(
263+
ResultsViewModel.Event.ToggleItemSelection(
264+
result.item,
265+
!isSelected,
266+
),
267+
)
268+
} else {
269+
onEvent(ResultsViewModel.Event.ResultClick(result.item))
270+
}
271+
},
272+
isSelected = isSelected,
273+
onSelectChange = { checked ->
279274
onEvent(
280275
ResultsViewModel.Event.ToggleItemSelection(
281276
result.item,
282-
true,
277+
checked,
283278
),
284279
)
285-
}
286-
},
287-
)
288-
HorizontalDivider(thickness = with(LocalDensity.current) { 1.toDp() })
280+
},
281+
onLongClick = {
282+
if (!isSelected) {
283+
onEvent(
284+
ResultsViewModel.Event.ToggleItemSelection(
285+
result.item,
286+
true,
287+
),
288+
)
289+
}
290+
},
291+
)
292+
HorizontalDivider(thickness = with(LocalDensity.current) { 1.toDp() })
293+
}
289294
}
290-
}
291-
if (state.areResultsLimited) {
292-
item("limited") {
293-
Text(
294-
text = stringResource(
295-
Res.string.Results_LimitedNotice,
296-
state.filter.limit,
297-
),
298-
style = MaterialTheme.typography.labelLarge,
299-
textAlign = TextAlign.Center,
300-
modifier = Modifier.fillMaxWidth()
301-
.padding(horizontal = 16.dp, vertical = 24.dp),
302-
)
295+
if (state.areResultsLimited) {
296+
item("limited") {
297+
Text(
298+
text = stringResource(
299+
Res.string.Results_LimitedNotice,
300+
state.filter.limit,
301+
),
302+
style = MaterialTheme.typography.labelLarge,
303+
textAlign = TextAlign.Center,
304+
modifier = Modifier.fillMaxWidth()
305+
.padding(horizontal = 16.dp, vertical = 24.dp),
306+
)
307+
}
303308
}
304309
}
310+
VerticalScrollbar(
311+
state = lazyListState,
312+
modifier = Modifier.align(Alignment.CenterEnd),
313+
)
305314
}
306315
}
307316
}

composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ class ResultsViewModel(
186186

187187
private val ResultListItem.monthAndYear
188188
get() = result.startTime.let { startTime ->
189-
LocalDate(year = startTime.year, month = startTime.month, dayOfMonth = 1)
189+
LocalDate(year = startTime.year, month = startTime.month, day = 1)
190190
}
191191

192192
data class State(

0 commit comments

Comments
 (0)