Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
880f5eb
Add StatsDataSource and refactor StatsRepository
adalpari Jan 22, 2026
b369322
Add StatsDataSource and refactor StatsRepository
adalpari Jan 22, 2026
4e745c3
Merge branch 'feat/CMM-1143-new-stats-views-card-tests-i2' of https:/…
adalpari Jan 22, 2026
45890d2
detekt
adalpari Jan 22, 2026
6b038ca
rename
adalpari Jan 22, 2026
d93b3f7
PR suggestions
adalpari Jan 22, 2026
3ea51d5
Adding period menu
adalpari Jan 22, 2026
8cc6738
Testing trunk push
adalpari Jan 22, 2026
13ca0ad
syncing selected period with the chart
adalpari Jan 22, 2026
6b5030e
Interval fixes
adalpari Jan 22, 2026
81b056e
Using async for period calls
adalpari Jan 22, 2026
6222c89
Using better naming
adalpari Jan 22, 2026
60766b8
Adding custom selection
adalpari Jan 22, 2026
949ba5f
Adding tests
adalpari Jan 22, 2026
cd2bf93
Merge branch 'trunk' into feat/CMM-1164-stats-Add-stats-period-selector
adalpari Jan 23, 2026
7bf1879
Extracting magic numbers
adalpari Jan 23, 2026
4e6327a
Some refactoring to avoid long functions
adalpari Jan 23, 2026
8500223
Extracting common examople code
adalpari Jan 23, 2026
44aaec8
Somo ktlint fixes
adalpari Jan 23, 2026
50d5fe6
Fixing month range labels
adalpari Jan 23, 2026
0804f2c
PR suggestions
adalpari Jan 23, 2026
f03262b
Custom range date selection improvements
adalpari Jan 23, 2026
d3b591d
Saving selected period in a more consistent way
adalpari Jan 23, 2026
23d7a81
Using the same datetime function in the repo as in the VM for consist…
adalpari Jan 23, 2026
dd2b563
Fixing tests
adalpari Jan 23, 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
Expand Up @@ -14,6 +14,10 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -29,7 +33,10 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -76,9 +83,24 @@ private enum class StatsTab(val titleResId: Int) {
private fun NewStatsScreen(
onBackPressed: () -> Unit
) {
val viewsStatsViewModel: ViewsStatsViewModel = viewModel()
val selectedPeriod by viewsStatsViewModel.selectedPeriod.collectAsState()

val tabs = StatsTab.entries
val pagerState = rememberPagerState(pageCount = { tabs.size })
val coroutineScope = rememberCoroutineScope()
var showPeriodMenu by remember { mutableStateOf(false) }
var showDateRangePicker by remember { mutableStateOf(false) }

if (showDateRangePicker) {
StatsDateRangePickerDialog(
onDismiss = { showDateRangePicker = false },
onDateRangeSelected = { startDate, endDate ->
viewsStatsViewModel.onPeriodChanged(StatsPeriod.Custom(startDate, endDate))
showDateRangePicker = false
}
)
}

Scaffold(
topBar = {
Expand All @@ -93,6 +115,31 @@ private fun NewStatsScreen(
contentDescription = stringResource(R.string.back)
)
}
},
actions = {
Box {
IconButton(onClick = { showPeriodMenu = true }) {
Icon(
imageVector = Icons.Default.DateRange,
contentDescription = stringResource(
R.string.stats_period_selector_content_description
)
)
}
StatsPeriodMenu(
expanded = showPeriodMenu,
selectedPeriod = selectedPeriod,
onDismiss = { showPeriodMenu = false },
onPresetSelected = { period ->
viewsStatsViewModel.onPeriodChanged(period)
showPeriodMenu = false
},
onCustomSelected = {
showPeriodMenu = false
showDateRangePicker = true
}
)
}
}
)
}
Expand Down Expand Up @@ -120,25 +167,25 @@ private fun NewStatsScreen(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
StatsTabContent(tab = tabs[page])
StatsTabContent(tab = tabs[page], viewsStatsViewModel = viewsStatsViewModel)
}
}
}
}

@Composable
private fun StatsTabContent(tab: StatsTab) {
private fun StatsTabContent(tab: StatsTab, viewsStatsViewModel: ViewsStatsViewModel) {
when (tab) {
StatsTab.TRAFFIC -> TrafficTabContent()
StatsTab.TRAFFIC -> TrafficTabContent(viewsStatsViewModel = viewsStatsViewModel)
else -> PlaceholderTabContent(tab)
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TrafficTabContent(
todaysStatsViewModel: TodaysStatsViewModel = viewModel(),
viewsStatsViewModel: ViewsStatsViewModel = viewModel()
viewsStatsViewModel: ViewsStatsViewModel,
todaysStatsViewModel: TodaysStatsViewModel = viewModel()
) {
val todaysStatsUiState by todaysStatsViewModel.uiState.collectAsState()
val viewsStatsUiState by viewsStatsViewModel.uiState.collectAsState()
Expand Down Expand Up @@ -189,6 +236,45 @@ private fun PlaceholderTabContent(tab: StatsTab) {
}
}

@Composable
private fun StatsPeriodMenu(
expanded: Boolean,
selectedPeriod: StatsPeriod,
onDismiss: () -> Unit,
onPresetSelected: (StatsPeriod) -> Unit,
onCustomSelected: () -> Unit
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismiss
) {
// Show preset periods
StatsPeriod.presets().forEach { period ->
val isSelected = selectedPeriod == period
DropdownMenuItem(
text = { Text(text = stringResource(id = period.labelResId)) },
onClick = { onPresetSelected(period) },
trailingIcon = if (isSelected) {
{ Icon(Icons.Default.Check, contentDescription = null) }
} else {
null
}
)
}
// Show Custom option
val isCustomSelected = selectedPeriod is StatsPeriod.Custom
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.stats_period_custom)) },
onClick = { onCustomSelected() },
trailingIcon = if (isCustomSelected) {
{ Icon(Icons.Default.Check, contentDescription = null) }
} else {
null
}
)
}
}

@Preview
@Composable
fun NewStatsScreenPreview() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.wordpress.android.ui.newstats

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DatePickerDefaults
import androidx.compose.material3.DateRangePicker
import androidx.compose.material3.DateRangePickerState
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SelectableDates
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDateRangePickerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.wordpress.android.R
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StatsDateRangePickerDialog(
onDismiss: () -> Unit,
onDateRangeSelected: (startDate: LocalDate, endDate: LocalDate) -> Unit
) {
val todayMillis = LocalDate.now()
.atStartOfDay(ZoneId.systemDefault())
.toInstant()
.toEpochMilli()

val dateRangePickerState = rememberDateRangePickerState(
initialDisplayMode = DisplayMode.Picker,
selectableDates = object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
return utcTimeMillis <= todayMillis
}
}
)

Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 6.dp
) {
DateRangePickerContent(
state = dateRangePickerState,
onDismiss = onDismiss,
onConfirm = {
val startMillis = dateRangePickerState.selectedStartDateMillis
val endMillis = dateRangePickerState.selectedEndDateMillis
if (startMillis != null && endMillis != null) {
val startDate = Instant.ofEpochMilli(startMillis)
.atZone(ZoneId.systemDefault())
.toLocalDate()
val endDate = Instant.ofEpochMilli(endMillis)
.atZone(ZoneId.systemDefault())
.toLocalDate()
// Ensure start date is before or equal to end date, swap if needed
if (startDate.isAfter(endDate)) {
onDateRangeSelected(endDate, startDate)
} else {
onDateRangeSelected(startDate, endDate)
}
}
onDismiss()
}
)
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DateRangePickerContent(
state: DateRangePickerState,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
val isConfirmEnabled = state.selectedStartDateMillis != null &&
state.selectedEndDateMillis != null

Column {
DateRangePicker(
state = state,
modifier = Modifier.heightIn(max = 500.dp),
title = {
Text(
text = stringResource(R.string.stats_select_date_range),
modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 16.dp)
)
},
showModeToggle = true,
colors = DatePickerDefaults.colors(
containerColor = MaterialTheme.colorScheme.surface
)
)

DialogButtons(
onDismiss = onDismiss,
onConfirm = onConfirm,
isConfirmEnabled = isConfirmEnabled
)
}
}

@Composable
private fun DialogButtons(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
isConfirmEnabled: Boolean
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
TextButton(
onClick = onConfirm,
enabled = isConfirmEnabled
) {
Text(stringResource(R.string.ok))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.wordpress.android.ui.newstats

import androidx.annotation.StringRes
import org.wordpress.android.R
import java.time.LocalDate

/**
* Represents the different time periods available for stats viewing.
*/
sealed class StatsPeriod(@StringRes val labelResId: Int) {
data object Today : StatsPeriod(R.string.stats_period_today)
data object Last7Days : StatsPeriod(R.string.stats_period_last_7_days)
data object Last30Days : StatsPeriod(R.string.stats_period_last_30_days)
data object Last6Months : StatsPeriod(R.string.stats_period_last_6_months)
data object Last12Months : StatsPeriod(R.string.stats_period_last_12_months)
data class Custom(val startDate: LocalDate, val endDate: LocalDate) :
StatsPeriod(R.string.stats_period_custom)

companion object {
/**
* Returns all preset periods (excluding Custom which requires dates).
*/
fun presets(): List<StatsPeriod> = listOf(
Today,
Last7Days,
Last30Days,
Last6Months,
Last12Months
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ interface StatsDataSource {
*/
enum class StatsUnit {
HOUR,
DAY
DAY,
WEEK,
MONTH
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,7 @@ class StatsDataSourceImpl @Inject constructor(
private fun StatsUnit.toApiUnit(): StatsVisitsUnit = when (this) {
StatsUnit.HOUR -> StatsVisitsUnit.HOUR
StatsUnit.DAY -> StatsVisitsUnit.DAY
StatsUnit.WEEK -> StatsVisitsUnit.WEEK
StatsUnit.MONTH -> StatsVisitsUnit.MONTH
}
}
Loading