feat: 时间选择器优化 —— 范围单弹窗选择(DateRangePicker)+ 打开回显 + 天/月选择器精简
This commit is contained in:
parent
777860024d
commit
3bef8cc3f1
@ -0,0 +1,79 @@
|
|||||||
|
package com.rainy.token.ui.dashboard
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.DatePickerDialog
|
||||||
|
import androidx.compose.material3.DateRangePicker
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
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 com.rainy.token.R
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期范围选择弹窗 —— 单弹窗内一次选出起止日期(连续高亮)。
|
||||||
|
*
|
||||||
|
* - 状态通过 "if (show) Dialog(...)" 条件组合创建:每次打开都会用最新预填值重新初始化;
|
||||||
|
* - 预填毫秒一律按 UTC 零点解释(与项目既有 UTC 语义一致);
|
||||||
|
* - 两端日期齐备前,确认按钮保持禁用(M3 范围选择:同一天点两次 = 单日区间)。
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun DateRangePickerDialog(
|
||||||
|
title: String,
|
||||||
|
initialStart: LocalDate?,
|
||||||
|
initialEnd: LocalDate?,
|
||||||
|
onConfirm: (LocalDate, LocalDate) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
// 预填数据 sanitize:无起点时忽略终点;终点早于起点时忽略终点
|
||||||
|
val sanitizedEnd = initialEnd?.takeIf { initialStart != null && !it.isBefore(initialStart) }
|
||||||
|
val state = rememberDateRangePickerState(
|
||||||
|
initialSelectedStartDateMillis = initialStart?.toUtcMillis(),
|
||||||
|
initialSelectedEndDateMillis = sanitizedEnd?.toUtcMillis()
|
||||||
|
)
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
val complete = state.selectedStartDateMillis != null && state.selectedEndDateMillis != null
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
val startMillis = state.selectedStartDateMillis
|
||||||
|
val endMillis = state.selectedEndDateMillis
|
||||||
|
if (startMillis != null && endMillis != null) {
|
||||||
|
onConfirm(startMillis.toUtcLocalDate(), endMillis.toUtcLocalDate())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = complete
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
DateRangePicker(
|
||||||
|
state = state,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(500.dp).padding(16.dp),
|
||||||
|
title = { Text(title) },
|
||||||
|
showModeToggle = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LocalDate.toUtcMillis(): Long =
|
||||||
|
atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()
|
||||||
|
|
||||||
|
private fun Long.toUtcLocalDate(): LocalDate =
|
||||||
|
Instant.ofEpochMilli(this).atOffset(ZoneOffset.UTC).toLocalDate()
|
||||||
@ -167,20 +167,21 @@ class UsageChartViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun setCustomDay(date: LocalDate) {
|
fun setCustomDay(date: LocalDate) {
|
||||||
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_DAY_HOURLY) }
|
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_DAY_HOURLY, customDay = date) }
|
||||||
_customDay = date
|
_customDay = date
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCustomMonth(monthDate: LocalDate) {
|
fun setCustomMonth(monthDate: LocalDate) {
|
||||||
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_MONTH_DAILY) }
|
val month = monthDate.withDayOfMonth(1)
|
||||||
_customMonth = monthDate.withDayOfMonth(1)
|
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_MONTH_DAILY, customMonth = month) }
|
||||||
|
_customMonth = month
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCustomRange(fromDate: LocalDate, toDate: LocalDate) {
|
fun setCustomRange(fromDate: LocalDate, toDate: LocalDate) {
|
||||||
if (toDate.isBefore(fromDate)) return
|
if (toDate.isBefore(fromDate)) return
|
||||||
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_RANGE_DAILY) }
|
_state.update { it.copy(granularity = ChartGranularity.CUSTOM_RANGE_DAILY, customRange = fromDate to toDate) }
|
||||||
_customRange = fromDate to toDate
|
_customRange = fromDate to toDate
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
@ -268,6 +269,10 @@ data class ChartUiState(
|
|||||||
val allModels: List<String> = emptyList(),
|
val allModels: List<String> = emptyList(),
|
||||||
/** 当前显示范围内实际出现过的模型(图例只显示这些) */
|
/** 当前显示范围内实际出现过的模型(图例只显示这些) */
|
||||||
val rangeModels: List<String> = emptyList(),
|
val rangeModels: List<String> = emptyList(),
|
||||||
|
/** 自定义选择的回显值(选择器打开时预填用) */
|
||||||
|
val customDay: LocalDate? = null,
|
||||||
|
val customMonth: LocalDate? = null,
|
||||||
|
val customRange: Pair<LocalDate, LocalDate>? = null,
|
||||||
val selectedModels: Set<String> = emptySet(), // empty = all
|
val selectedModels: Set<String> = emptySet(), // empty = all
|
||||||
val buckets: List<ChartBucket> = emptyList(),
|
val buckets: List<ChartBucket> = emptyList(),
|
||||||
val useUtc8: Boolean = false,
|
val useUtc8: Boolean = false,
|
||||||
|
|||||||
@ -59,6 +59,7 @@ import com.rainy.token.ui.theme.inkMuted
|
|||||||
import com.rainy.token.ui.theme.StrawberryPink
|
import com.rainy.token.ui.theme.StrawberryPink
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -82,10 +83,7 @@ fun UsageDataScreen(
|
|||||||
|
|
||||||
var timeMenuExpanded by remember { mutableStateOf(false) }
|
var timeMenuExpanded by remember { mutableStateOf(false) }
|
||||||
var modelMenuExpanded by remember { mutableStateOf(false) }
|
var modelMenuExpanded by remember { mutableStateOf(false) }
|
||||||
var showStartPicker by remember { mutableStateOf(false) }
|
var showRangePicker by remember { mutableStateOf(false) }
|
||||||
var showEndPicker by remember { mutableStateOf(false) }
|
|
||||||
var customStartMs by remember { mutableStateOf(0L) }
|
|
||||||
var customEndMs by remember { mutableStateOf(0L) }
|
|
||||||
var showCustomDayPicker by remember { mutableStateOf(false) }
|
var showCustomDayPicker by remember { mutableStateOf(false) }
|
||||||
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
||||||
var rawRecord by remember { mutableStateOf<UsageRecord?>(null) }
|
var rawRecord by remember { mutableStateOf<UsageRecord?>(null) }
|
||||||
@ -127,7 +125,7 @@ fun UsageDataScreen(
|
|||||||
timeMenuExpanded = false; showCustomMonthPicker = true
|
timeMenuExpanded = false; showCustomMonthPicker = true
|
||||||
})
|
})
|
||||||
DropdownMenuItem(text = { Text(stringResource(R.string.time_custom_range)) }, onClick = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.time_custom_range)) }, onClick = {
|
||||||
timeMenuExpanded = false; viewModel.setTimeFilter(TimeFilter.Custom(0L, 0L))
|
timeMenuExpanded = false; showRangePicker = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,11 +153,8 @@ fun UsageDataScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义时间范围
|
// 自定义时间范围:确认选择后显示 UTC 说明
|
||||||
if (state.timeFilter is TimeFilter.Custom && (state.timeFilter as TimeFilter.Custom).from == 0L) {
|
if (state.timeFilter is TimeFilter.Custom) {
|
||||||
CustomTimeRangeRow(customStartMs, customEndMs,
|
|
||||||
{ showStartPicker = true }, { showEndPicker = true },
|
|
||||||
{ viewModel.setTimeFilter(TimeFilter.Custom(customStartMs, customEndMs)) })
|
|
||||||
Text(stringResource(R.string.usage_utc0_note), style = MaterialTheme.typography.bodySmall, color = inkMuted())
|
Text(stringResource(R.string.usage_utc0_note), style = MaterialTheme.typography.bodySmall, color = inkMuted())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,20 +223,43 @@ fun UsageDataScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 日期选择器
|
// 日期选择器
|
||||||
if (showCustomDayPicker) DateTimePickerDialog(stringResource(R.string.date_select_day), { ms ->
|
if (showCustomDayPicker) DateOnlyPickerDialog(
|
||||||
val utc = ZoneOffset.UTC; val ds = Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate().atStartOfDay(utc).toInstant().toEpochMilli()
|
title = stringResource(R.string.date_select_day),
|
||||||
viewModel.setTimeFilter(TimeFilter.Custom(ds, ds + 86400_000L - 1)); showCustomDayPicker = false
|
initialDate = (state.timeFilter as? TimeFilter.Custom)?.from?.takeIf { it > 0 }?.toUtcLocalDate(),
|
||||||
}, { showCustomDayPicker = false })
|
onConfirm = { date ->
|
||||||
if (showCustomMonthPicker) DateTimePickerDialog(stringResource(R.string.date_select_month), { ms ->
|
val ds = date.toUtcStartOfDayMillis()
|
||||||
val utc = ZoneOffset.UTC; val ld = Instant.ofEpochMilli(ms).atOffset(utc).toLocalDate()
|
viewModel.setTimeFilter(TimeFilter.Custom(ds, ds + 86400_000L - 1))
|
||||||
val msStart = ld.withDayOfMonth(1).atStartOfDay(utc).toInstant().toEpochMilli()
|
showCustomDayPicker = false
|
||||||
val msEnd = ld.withDayOfMonth(ld.lengthOfMonth()).plusDays(1).atStartOfDay(utc).toInstant().toEpochMilli() - 1
|
},
|
||||||
viewModel.setTimeFilter(TimeFilter.Custom(msStart, msEnd)); showCustomMonthPicker = false
|
onDismiss = { showCustomDayPicker = false }
|
||||||
}, { showCustomMonthPicker = false })
|
)
|
||||||
if (showStartPicker) DateTimePickerDialog(stringResource(R.string.date_start), { customStartMs = it; showStartPicker = false }, { showStartPicker = false })
|
if (showCustomMonthPicker) DateOnlyPickerDialog(
|
||||||
if (showEndPicker) DateTimePickerDialog(stringResource(R.string.date_end), { ms ->
|
title = stringResource(R.string.date_select_month),
|
||||||
viewModel.setTimeFilter(TimeFilter.Custom(customStartMs, ms)); showEndPicker = false
|
initialDate = (state.timeFilter as? TimeFilter.Custom)?.from?.takeIf { it > 0 }?.toUtcLocalDate(),
|
||||||
}, { showEndPicker = false })
|
onConfirm = { date ->
|
||||||
|
val utc = ZoneOffset.UTC
|
||||||
|
val msStart = date.withDayOfMonth(1).atStartOfDay(utc).toInstant().toEpochMilli()
|
||||||
|
val msEnd = date.withDayOfMonth(date.lengthOfMonth()).plusDays(1).atStartOfDay(utc).toInstant().toEpochMilli() - 1
|
||||||
|
viewModel.setTimeFilter(TimeFilter.Custom(msStart, msEnd))
|
||||||
|
showCustomMonthPicker = false
|
||||||
|
},
|
||||||
|
onDismiss = { showCustomMonthPicker = false }
|
||||||
|
)
|
||||||
|
if (showRangePicker) {
|
||||||
|
val current = state.timeFilter as? TimeFilter.Custom
|
||||||
|
DateRangePickerDialog(
|
||||||
|
title = stringResource(R.string.date_pick_range),
|
||||||
|
initialStart = current?.from?.takeIf { it > 0 }?.toUtcLocalDate(),
|
||||||
|
initialEnd = current?.to?.takeIf { current.from > 0 }?.toUtcLocalDate(),
|
||||||
|
onConfirm = { from, to ->
|
||||||
|
val fromMs = from.toUtcStartOfDayMillis()
|
||||||
|
val toMs = to.toUtcStartOfDayMillis()
|
||||||
|
viewModel.setTimeFilter(TimeFilter.Custom(fromMs, toMs + 86400_000L - 1))
|
||||||
|
showRangePicker = false
|
||||||
|
},
|
||||||
|
onDismiss = { showRangePicker = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 原始数据弹窗
|
// 原始数据弹窗
|
||||||
rawRecord?.let { record ->
|
rawRecord?.let { record ->
|
||||||
@ -313,6 +331,12 @@ private fun RawField(label: String, value: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Long.toUtcLocalDate(): LocalDate =
|
||||||
|
Instant.ofEpochMilli(this).atOffset(ZoneOffset.UTC).toLocalDate()
|
||||||
|
|
||||||
|
private fun LocalDate.toUtcStartOfDayMillis(): Long =
|
||||||
|
atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()
|
||||||
|
|
||||||
private fun formatInputWithCache(record: UsageRecord): String {
|
private fun formatInputWithCache(record: UsageRecord): String {
|
||||||
val input = record.inputTokens + record.cacheReadTokens
|
val input = record.inputTokens + record.cacheReadTokens
|
||||||
val cache = record.cacheReadTokens
|
val cache = record.cacheReadTokens
|
||||||
|
|||||||
@ -97,9 +97,7 @@ fun UsageDetailScreen(
|
|||||||
var modelExpanded by remember { mutableStateOf(false) }
|
var modelExpanded by remember { mutableStateOf(false) }
|
||||||
var showCustomDayPicker by remember { mutableStateOf(false) }
|
var showCustomDayPicker by remember { mutableStateOf(false) }
|
||||||
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
var showCustomMonthPicker by remember { mutableStateOf(false) }
|
||||||
var showCustomRangeStart by remember { mutableStateOf(false) }
|
var showCustomRangePicker by remember { mutableStateOf(false) }
|
||||||
var showCustomRangeEnd by remember { mutableStateOf(false) }
|
|
||||||
var customRangeStartDate by remember { mutableStateOf<LocalDate?>(null) }
|
|
||||||
var showCostDetail by remember { mutableStateOf(false) }
|
var showCostDetail by remember { mutableStateOf(false) }
|
||||||
var showReqDetail by remember { mutableStateOf(false) }
|
var showReqDetail by remember { mutableStateOf(false) }
|
||||||
var showTokenDetail by remember { mutableStateOf(false) }
|
var showTokenDetail by remember { mutableStateOf(false) }
|
||||||
@ -170,7 +168,7 @@ fun UsageDetailScreen(
|
|||||||
when (g) {
|
when (g) {
|
||||||
ChartGranularity.CUSTOM_DAY_HOURLY -> showCustomDayPicker = true
|
ChartGranularity.CUSTOM_DAY_HOURLY -> showCustomDayPicker = true
|
||||||
ChartGranularity.CUSTOM_MONTH_DAILY -> showCustomMonthPicker = true
|
ChartGranularity.CUSTOM_MONTH_DAILY -> showCustomMonthPicker = true
|
||||||
ChartGranularity.CUSTOM_RANGE_DAILY -> showCustomRangeStart = true
|
ChartGranularity.CUSTOM_RANGE_DAILY -> showCustomRangePicker = true
|
||||||
else -> viewModel.setGranularity(g)
|
else -> viewModel.setGranularity(g)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -321,31 +319,35 @@ fun UsageDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 自定义日期选择器
|
// 自定义日期选择器
|
||||||
if (showCustomDayPicker) DateOnlyPickerDialog(stringResource(R.string.date_select_day), { date ->
|
if (showCustomDayPicker) DateOnlyPickerDialog(
|
||||||
viewModel.setCustomDay(date)
|
title = stringResource(R.string.date_select_day),
|
||||||
showCustomDayPicker = false
|
initialDate = state.customDay,
|
||||||
}, { showCustomDayPicker = false })
|
onConfirm = { date ->
|
||||||
if (showCustomMonthPicker) DateOnlyPickerDialog(stringResource(R.string.date_select_month), { date ->
|
viewModel.setCustomDay(date)
|
||||||
viewModel.setCustomMonth(date)
|
showCustomDayPicker = false
|
||||||
showCustomMonthPicker = false
|
},
|
||||||
}, { showCustomMonthPicker = false })
|
onDismiss = { showCustomDayPicker = false }
|
||||||
if (showCustomRangeStart) DateOnlyPickerDialog(stringResource(R.string.date_pick_start), { date ->
|
)
|
||||||
customRangeStartDate = date
|
if (showCustomMonthPicker) DateOnlyPickerDialog(
|
||||||
showCustomRangeStart = false
|
title = stringResource(R.string.date_select_month),
|
||||||
showCustomRangeEnd = true
|
initialDate = state.customMonth,
|
||||||
}, { showCustomRangeStart = false })
|
onConfirm = { date ->
|
||||||
if (showCustomRangeEnd) {
|
viewModel.setCustomMonth(date)
|
||||||
val startDate = customRangeStartDate
|
showCustomMonthPicker = false
|
||||||
DateOnlyPickerDialog(
|
},
|
||||||
title = stringResource(R.string.date_pick_end),
|
onDismiss = { showCustomMonthPicker = false }
|
||||||
onConfirm = { endDate ->
|
)
|
||||||
if (startDate != null && !endDate.isBefore(startDate)) {
|
if (showCustomRangePicker) {
|
||||||
viewModel.setCustomRange(startDate, endDate)
|
val currentRange = state.customRange
|
||||||
showCustomRangeEnd = false
|
DateRangePickerDialog(
|
||||||
}
|
title = stringResource(R.string.date_pick_range),
|
||||||
|
initialStart = currentRange?.first,
|
||||||
|
initialEnd = currentRange?.second,
|
||||||
|
onConfirm = { from, to ->
|
||||||
|
viewModel.setCustomRange(from, to)
|
||||||
|
showCustomRangePicker = false
|
||||||
},
|
},
|
||||||
onDismiss = { showCustomRangeEnd = false },
|
onDismiss = { showCustomRangePicker = false }
|
||||||
minDate = startDate
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val models = state.rangeModels
|
val models = state.rangeModels
|
||||||
@ -465,8 +467,9 @@ internal fun CustomTimeRangeRow(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DateOnlyPickerDialog(
|
internal fun DateOnlyPickerDialog(
|
||||||
title: String,
|
title: String,
|
||||||
|
initialDate: LocalDate? = null,
|
||||||
onConfirm: (LocalDate) -> Unit,
|
onConfirm: (LocalDate) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
minDate: LocalDate? = null
|
minDate: LocalDate? = null
|
||||||
@ -480,7 +483,10 @@ private fun DateOnlyPickerDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val dateState = rememberDatePickerState(selectableDates = selectableDates)
|
val dateState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = initialDate?.atStartOfDay(utc)?.toInstant()?.toEpochMilli(),
|
||||||
|
selectableDates = selectableDates
|
||||||
|
)
|
||||||
|
|
||||||
DatePickerDialog(
|
DatePickerDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@ -512,7 +518,7 @@ private fun DateOnlyPickerDialog(
|
|||||||
color = InkMuted
|
color = InkMuted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DatePicker(state = dateState)
|
DatePicker(state = dateState, title = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,9 @@ import com.rainy.token.data.local.OverviewStats
|
|||||||
import com.rainy.token.ui.theme.inkMuted
|
import com.rainy.token.ui.theme.inkMuted
|
||||||
import com.rainy.token.ui.theme.StrawberryPink
|
import com.rainy.token.ui.theme.StrawberryPink
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneOffset
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@ -74,10 +77,7 @@ fun UsageOverviewScreen(
|
|||||||
|
|
||||||
var menuExpanded by remember { mutableStateOf(false) }
|
var menuExpanded by remember { mutableStateOf(false) }
|
||||||
var modelMenuExpanded by remember { mutableStateOf(false) }
|
var modelMenuExpanded by remember { mutableStateOf(false) }
|
||||||
var showStartPicker by remember { mutableStateOf(false) }
|
var showRangePicker by remember { mutableStateOf(false) }
|
||||||
var showEndPicker by remember { mutableStateOf(false) }
|
|
||||||
var customStartMs by remember { mutableStateOf(0L) }
|
|
||||||
var customEndMs by remember { mutableStateOf(0L) }
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
@ -115,16 +115,13 @@ fun UsageOverviewScreen(
|
|||||||
}
|
}
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.time_custom)) },
|
text = { Text(stringResource(R.string.time_custom)) },
|
||||||
onClick = { menuExpanded = false; viewModel.setTimeFilter(TimeFilter.Custom(0L, 0L)) }
|
onClick = { menuExpanded = false; showRangePicker = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uiState.timeFilter is TimeFilter.Custom) {
|
if (uiState.timeFilter is TimeFilter.Custom) {
|
||||||
item {
|
item {
|
||||||
CustomTimeRangeRow(customStartMs, customEndMs,
|
|
||||||
{ showStartPicker = true }, { showEndPicker = true },
|
|
||||||
{ viewModel.setTimeFilter(TimeFilter.Custom(customStartMs, customEndMs)) })
|
|
||||||
Text(stringResource(R.string.usage_utc0_note), style = MaterialTheme.typography.bodySmall, color = inkMuted())
|
Text(stringResource(R.string.usage_utc0_note), style = MaterialTheme.typography.bodySmall, color = inkMuted())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,8 +176,21 @@ fun UsageOverviewScreen(
|
|||||||
}
|
}
|
||||||
item { Spacer(Modifier.height(24.dp)) }
|
item { Spacer(Modifier.height(24.dp)) }
|
||||||
}
|
}
|
||||||
if (showStartPicker) DateTimePickerDialog(stringResource(R.string.date_start), { customStartMs = it; showStartPicker = false }, { showStartPicker = false })
|
if (showRangePicker) {
|
||||||
if (showEndPicker) DateTimePickerDialog(stringResource(R.string.date_end), { customEndMs = it; showEndPicker = false }, { showEndPicker = false })
|
val current = uiState.timeFilter as? TimeFilter.Custom
|
||||||
|
DateRangePickerDialog(
|
||||||
|
title = stringResource(R.string.date_pick_range),
|
||||||
|
initialStart = current?.from?.takeIf { it > 0 }?.toUtcLocalDate(),
|
||||||
|
initialEnd = current?.to?.takeIf { current.from > 0 }?.toUtcLocalDate(),
|
||||||
|
onConfirm = { from, to ->
|
||||||
|
val fromMs = from.toUtcStartOfDayMillis()
|
||||||
|
val toMs = to.toUtcStartOfDayMillis()
|
||||||
|
viewModel.setTimeFilter(TimeFilter.Custom(fromMs, toMs + 86400_000L - 1))
|
||||||
|
showRangePicker = false
|
||||||
|
},
|
||||||
|
onDismiss = { showRangePicker = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,6 +291,12 @@ fun UsageOverviewScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Long.toUtcLocalDate(): LocalDate =
|
||||||
|
Instant.ofEpochMilli(this).atOffset(ZoneOffset.UTC).toLocalDate()
|
||||||
|
|
||||||
|
private fun LocalDate.toUtcStartOfDayMillis(): Long =
|
||||||
|
atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()
|
||||||
|
|
||||||
private fun formatTokenCount(tokens: Long): String = when {
|
private fun formatTokenCount(tokens: Long): String = when {
|
||||||
tokens >= 1_000_000 -> "${"%.1f".format(Locale.US, tokens / 1_000_000.0)}M"
|
tokens >= 1_000_000 -> "${"%.1f".format(Locale.US, tokens / 1_000_000.0)}M"
|
||||||
tokens >= 1_000 -> "${"%.1f".format(Locale.US, tokens / 1_000.0)}K"
|
tokens >= 1_000 -> "${"%.1f".format(Locale.US, tokens / 1_000.0)}K"
|
||||||
|
|||||||
@ -341,6 +341,7 @@
|
|||||||
<string name="count_times">%1$d次</string>
|
<string name="count_times">%1$d次</string>
|
||||||
<string name="date_select_day">选择日期</string>
|
<string name="date_select_day">选择日期</string>
|
||||||
<string name="date_select_month">选择月份(任意一天)</string>
|
<string name="date_select_month">选择月份(任意一天)</string>
|
||||||
|
<string name="date_pick_range">日期范围</string>
|
||||||
<string name="date_pick_start">开始日期</string>
|
<string name="date_pick_start">开始日期</string>
|
||||||
<string name="date_pick_end">结束日期</string>
|
<string name="date_pick_end">结束日期</string>
|
||||||
<string name="chart_detail_cost">消费明细</string>
|
<string name="chart_detail_cost">消费明细</string>
|
||||||
|
|||||||
@ -341,6 +341,7 @@
|
|||||||
<string name="count_times">%1$d次</string>
|
<string name="count_times">%1$d次</string>
|
||||||
<string name="date_select_day">選擇日期</string>
|
<string name="date_select_day">選擇日期</string>
|
||||||
<string name="date_select_month">選擇月份(任意一天)</string>
|
<string name="date_select_month">選擇月份(任意一天)</string>
|
||||||
|
<string name="date_pick_range">日期範圍</string>
|
||||||
<string name="date_pick_start">開始日期</string>
|
<string name="date_pick_start">開始日期</string>
|
||||||
<string name="date_pick_end">結束日期</string>
|
<string name="date_pick_end">結束日期</string>
|
||||||
<string name="chart_detail_cost">消費明細</string>
|
<string name="chart_detail_cost">消費明細</string>
|
||||||
|
|||||||
@ -344,6 +344,7 @@
|
|||||||
<string name="count_times">%1$d calls</string>
|
<string name="count_times">%1$d calls</string>
|
||||||
<string name="date_select_day">Pick a date</string>
|
<string name="date_select_day">Pick a date</string>
|
||||||
<string name="date_select_month">Pick a month (any day)</string>
|
<string name="date_select_month">Pick a month (any day)</string>
|
||||||
|
<string name="date_pick_range">Date range</string>
|
||||||
<string name="date_pick_start">Start date</string>
|
<string name="date_pick_start">Start date</string>
|
||||||
<string name="date_pick_end">End date</string>
|
<string name="date_pick_end">End date</string>
|
||||||
<string name="chart_detail_cost">Spending breakdown</string>
|
<string name="chart_detail_cost">Spending breakdown</string>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user