feat: формат даты/времени и pull-to-refresh на Обзоре (0.5.1)

Локальный вывод дат (по умолчанию ДД/ММ/ГГГГ ЧЧ:мм:сс), выбор формата в Устройство,
дата напротив событий на Обзоре, исправление compile и жеста обновления.
This commit is contained in:
PTah
2026-06-14 17:48:48 +10:00
parent 6af2a96c38
commit 180403068e
8 changed files with 179 additions and 16 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "ru.kalinamall.seaca" applicationId = "ru.kalinamall.seaca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 14 versionCode = 15
versionName = "0.5.0" versionName = "0.5.1"
} }
buildTypes { buildTypes {
@@ -30,6 +30,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf as composeMutableStateOf import androidx.compose.runtime.mutableStateOf as composeMutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -0,0 +1,18 @@
package ru.kalinamall.seaca.data
enum class DateTimeDisplayMode(
val storageKey: String,
val label: String,
val pattern: String?,
) {
SLASH("slash", "ДД/ММ/ГГГГ ЧЧ:мм:сс", "dd/MM/yyyy HH:mm:ss"),
DASH("dash", "ДД-ММ-ГГГГ ЧЧ:мм:сс", "dd-MM-yyyy HH:mm:ss"),
DOT("dot", "ДД.ММ.ГГГГ ЧЧ:мм:сс", "dd.MM.yyyy HH:mm:ss"),
LOCALE("locale", "Как в системе телефона", null),
;
companion object {
fun fromStorage(value: String?): DateTimeDisplayMode =
entries.firstOrNull { it.storageKey == value } ?: SLASH
}
}
@@ -36,13 +36,24 @@ class SessionStore(private val context: Context) {
NotificationMode.fromStorage(prefs[KEY_NOTIFICATION_MODE]) NotificationMode.fromStorage(prefs[KEY_NOTIFICATION_MODE])
} }
val dateTimeDisplayMode: Flow<DateTimeDisplayMode> = context.dataStore.data.map { prefs ->
DateTimeDisplayMode.fromStorage(prefs[KEY_DATE_TIME_FORMAT])
}
suspend fun getNotificationMode(): NotificationMode = suspend fun getNotificationMode(): NotificationMode =
NotificationMode.fromStorage(context.dataStore.data.first()[KEY_NOTIFICATION_MODE]) NotificationMode.fromStorage(context.dataStore.data.first()[KEY_NOTIFICATION_MODE])
suspend fun getDateTimeDisplayMode(): DateTimeDisplayMode =
DateTimeDisplayMode.fromStorage(context.dataStore.data.first()[KEY_DATE_TIME_FORMAT])
suspend fun setNotificationMode(mode: NotificationMode) { suspend fun setNotificationMode(mode: NotificationMode) {
context.dataStore.edit { it[KEY_NOTIFICATION_MODE] = mode.storageKey } context.dataStore.edit { it[KEY_NOTIFICATION_MODE] = mode.storageKey }
} }
suspend fun setDateTimeDisplayMode(mode: DateTimeDisplayMode) {
context.dataStore.edit { it[KEY_DATE_TIME_FORMAT] = mode.storageKey }
}
suspend fun getBaseUrl(): String? = context.dataStore.data.first()[KEY_BASE_URL] suspend fun getBaseUrl(): String? = context.dataStore.data.first()[KEY_BASE_URL]
suspend fun trustAllCerts(): Boolean = context.dataStore.data.first()[KEY_TRUST_ALL] ?: false suspend fun trustAllCerts(): Boolean = context.dataStore.data.first()[KEY_TRUST_ALL] ?: false
@@ -102,6 +113,7 @@ class SessionStore(private val context: Context) {
private val KEY_DEVICE_UUID = stringPreferencesKey("device_uuid") private val KEY_DEVICE_UUID = stringPreferencesKey("device_uuid")
private val KEY_TRUST_ALL = booleanPreferencesKey("trust_all_certs") private val KEY_TRUST_ALL = booleanPreferencesKey("trust_all_certs")
private val KEY_NOTIFICATION_MODE = stringPreferencesKey("notification_mode") private val KEY_NOTIFICATION_MODE = stringPreferencesKey("notification_mode")
private val KEY_DATE_TIME_FORMAT = stringPreferencesKey("date_time_format")
private const val KEY_ACCESS = "access_token" private const val KEY_ACCESS = "access_token"
private const val KEY_REFRESH = "refresh_token" private const val KEY_REFRESH = "refresh_token"
private const val KEY_DEVICE_ID = "device_id" private const val KEY_DEVICE_ID = "device_id"
@@ -16,6 +16,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -42,11 +43,14 @@ import ru.kalinamall.seaca.R
import ru.kalinamall.seaca.data.EventSummary import ru.kalinamall.seaca.data.EventSummary
import ru.kalinamall.seaca.data.EventDetail import ru.kalinamall.seaca.data.EventDetail
import ru.kalinamall.seaca.data.HostDetail import ru.kalinamall.seaca.data.HostDetail
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.kalinamall.seaca.data.DateTimeDisplayMode
import ru.kalinamall.seaca.data.NotificationMode import ru.kalinamall.seaca.data.NotificationMode
import ru.kalinamall.seaca.data.ProblemDetail import ru.kalinamall.seaca.data.ProblemDetail
import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.data.SacRepository
import ru.kalinamall.seaca.ui.components.BackArrowButton import ru.kalinamall.seaca.ui.components.BackArrowButton
import ru.kalinamall.seaca.ui.components.ForwardArrowButton import ru.kalinamall.seaca.ui.components.ForwardArrowButton
import ru.kalinamall.seaca.ui.util.SacDateTimes
import ru.kalinamall.seaca.ui.util.dailyReportTypeLabel import ru.kalinamall.seaca.ui.util.dailyReportTypeLabel
import ru.kalinamall.seaca.ui.util.formatEventDetails import ru.kalinamall.seaca.ui.util.formatEventDetails
import ru.kalinamall.seaca.ui.util.inventoryFieldLines import ru.kalinamall.seaca.ui.util.inventoryFieldLines
@@ -63,6 +67,9 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) {
var data by remember { mutableStateOf<EventDetail?>(null) } var data by remember { mutableStateOf<EventDetail?>(null) }
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
var loading by remember { mutableStateOf(true) } var loading by remember { mutableStateOf(true) }
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH,
)
LaunchedEffect(id) { LaunchedEffect(id) {
loading = true loading = true
@@ -83,7 +90,7 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) {
if (isDailyReportType(d.type)) dailyReportTypeLabel(d.type) else d.type, if (isDailyReportType(d.type)) dailyReportTypeLabel(d.type) else d.type,
) )
Field("Хост", d.hostname) Field("Хост", d.hostname)
Field("Время", d.occurredAt) Field("Время", SacDateTimes.format(d.occurredAt, dateMode))
Field("Кратко", d.summary) Field("Кратко", d.summary)
if (isDailyReportType(d.type)) { if (isDailyReportType(d.type)) {
reportTextFromDetails(d.details)?.let { MultilineField("Отчёт", it) } reportTextFromDetails(d.details)?.let { MultilineField("Отчёт", it) }
@@ -126,6 +133,9 @@ fun ProblemDetailScreen(
var loading by remember { mutableStateOf(true) } var loading by remember { mutableStateOf(true) }
var acting by remember { mutableStateOf(false) } var acting by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH,
)
fun load() { fun load() {
scope.launch { scope.launch {
@@ -192,7 +202,11 @@ fun ProblemDetailScreen(
if (d.events.isNotEmpty()) { if (d.events.isNotEmpty()) {
Text("Связанные события", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface) Text("Связанные события", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
d.events.forEach { e -> d.events.forEach { e ->
Text("${e.severity} · ${e.title}\n${e.occurredAt}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(
"${e.severity} · ${e.title}\n${SacDateTimes.format(e.occurredAt, dateMode)}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} }
} }
} }
@@ -209,6 +223,9 @@ fun HostDetailScreen(
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
var loading by remember { mutableStateOf(true) } var loading by remember { mutableStateOf(true) }
var selectedTab by remember { mutableIntStateOf(HostDetailTab.Info.ordinal) } var selectedTab by remember { mutableIntStateOf(HostDetailTab.Info.ordinal) }
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH,
)
LaunchedEffect(id) { LaunchedEffect(id) {
loading = true loading = true
@@ -264,7 +281,7 @@ fun HostDetailScreen(
} }
} }
when (HostDetailTab.entries[selectedTab]) { when (HostDetailTab.entries[selectedTab]) {
HostDetailTab.Info -> HostInfoContent(host) HostDetailTab.Info -> HostInfoContent(host, dateMode)
HostDetailTab.Events -> HostEventsContent( HostDetailTab.Events -> HostEventsContent(
repo = repo, repo = repo,
hostname = host.hostname, hostname = host.hostname,
@@ -276,7 +293,7 @@ fun HostDetailScreen(
} }
@Composable @Composable
private fun HostInfoContent(host: HostDetail) { private fun HostInfoContent(host: HostDetail, dateMode: DateTimeDisplayMode) {
Column( Column(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
@@ -288,7 +305,7 @@ private fun HostInfoContent(host: HostDetail) {
Field("Версия", host.productVersion ?: "") Field("Версия", host.productVersion ?: "")
Field("ОС", "${host.osFamily} ${host.osVersion.orEmpty()}".trim()) Field("ОС", "${host.osFamily} ${host.osVersion.orEmpty()}".trim())
Field("Агент", host.agentStatus) Field("Агент", host.agentStatus)
Field("Последний контакт", host.lastSeenAt) Field("Последний контакт", SacDateTimes.format(host.lastSeenAt, dateMode))
host.inventory?.let { inv -> host.inventory?.let { inv ->
val lines = inventoryFieldLines(inv) val lines = inventoryFieldLines(inv)
if (lines.isEmpty()) { if (lines.isEmpty()) {
@@ -301,6 +318,7 @@ private fun HostInfoContent(host: HostDetail) {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun HostEventsContent( private fun HostEventsContent(
repo: SacRepository, repo: SacRepository,
@@ -314,6 +332,9 @@ private fun HostEventsContent(
var refreshing by remember { mutableStateOf(false) } var refreshing by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH,
)
fun load(showRefreshIndicator: Boolean = false) { fun load(showRefreshIndicator: Boolean = false) {
scope.launch { scope.launch {
@@ -369,7 +390,7 @@ private fun HostEventsContent(
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
Text( Text(
event.occurredAt, SacDateTimes.format(event.occurredAt, dateMode),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -398,6 +419,9 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
var role by remember { mutableStateOf<String?>(null) } var role by remember { mutableStateOf<String?>(null) }
var deviceId by remember { mutableStateOf(0) } var deviceId by remember { mutableStateOf(0) }
val notificationMode by repo.session.notificationMode.collectAsState(initial = NotificationMode.PUSH_SOUND) val notificationMode by repo.session.notificationMode.collectAsState(initial = NotificationMode.PUSH_SOUND)
val dateTimeMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH,
)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -458,6 +482,34 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
} }
} }
Text(
stringResource(R.string.date_time_format_title),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 8.dp),
)
DateTimeDisplayMode.entries.forEach { mode ->
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
selected = dateTimeMode == mode,
onClick = {
scope.launch { repo.session.setDateTimeDisplayMode(mode) }
},
)
Text(
mode.label,
modifier = Modifier.padding(start = 8.dp),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
Button( Button(
onClick = { onClick = {
scope.launch { scope.launch {
@@ -1,6 +1,8 @@
package ru.kalinamall.seaca.ui.screens package ru.kalinamall.seaca.ui.screens
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -33,8 +35,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.kalinamall.seaca.data.DateTimeDisplayMode
import ru.kalinamall.seaca.data.DashboardSummary import ru.kalinamall.seaca.data.DashboardSummary
import ru.kalinamall.seaca.data.EventListResponse import ru.kalinamall.seaca.data.EventListResponse
import ru.kalinamall.seaca.data.EventSummary import ru.kalinamall.seaca.data.EventSummary
@@ -43,6 +47,7 @@ import ru.kalinamall.seaca.data.ProblemSummary
import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.data.SacRepository
import ru.kalinamall.seaca.ui.components.BackArrowButton import ru.kalinamall.seaca.ui.components.BackArrowButton
import ru.kalinamall.seaca.ui.components.ForwardArrowButton import ru.kalinamall.seaca.ui.components.ForwardArrowButton
import ru.kalinamall.seaca.ui.util.SacDateTimes
private val REPORT_TYPE_OPTIONS = listOf( private val REPORT_TYPE_OPTIONS = listOf(
"report.daily.ssh" to "Отчёты ssh клиентов", "report.daily.ssh" to "Отчёты ssh клиентов",
@@ -57,6 +62,10 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
var loading by remember { mutableStateOf(false) } var loading by remember { mutableStateOf(false) }
var refreshing by remember { mutableStateOf(false) } var refreshing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH,
)
val scrollState = rememberScrollState()
fun load(showRefreshIndicator: Boolean = false) { fun load(showRefreshIndicator: Boolean = false) {
scope.launch { scope.launch {
@@ -89,7 +98,13 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
onRefresh = { load(showRefreshIndicator = true) }, onRefresh = { load(showRefreshIndicator = true) },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
error?.let { Text(it, color = MaterialTheme.colorScheme.error) } error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
data?.let { d -> data?.let { d ->
StatCard("События 24ч", d.eventsLast24h.toString()) StatCard("События 24ч", d.eventsLast24h.toString())
@@ -101,11 +116,23 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
d.recentEvents.take(8).forEach { e -> d.recentEvents.take(8).forEach { e ->
Text( Row(
dashboardRecentEventLine(e), Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodySmall, horizontalArrangement = Arrangement.spacedBy(8.dp),
color = MaterialTheme.colorScheme.onSurface, verticalAlignment = Alignment.Top,
) ) {
Text(
dashboardRecentEventLine(e),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f),
)
Text(
SacDateTimes.format(e.occurredAt, dateMode),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} }
} }
if (data == null && loading) { if (data == null && loading) {
@@ -144,6 +171,9 @@ private fun StatCard(label: String, value: String) {
@Composable @Composable
fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) { fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
var hostname by remember { mutableStateOf("") } var hostname by remember { mutableStateOf("") }
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH,
)
PagedListScreen( PagedListScreen(
title = "События", title = "События",
@@ -160,7 +190,7 @@ fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
}, },
itemKey = EventSummary::id, itemKey = EventSummary::id,
itemLabel = { "${it.severity} · ${it.title}" }, itemLabel = { "${it.severity} · ${it.title}" },
itemSub = { "${it.hostname} · ${it.occurredAt}" }, itemSub = { "${it.hostname} · ${SacDateTimes.format(it.occurredAt, dateMode)}" },
onOpen = onOpen, onOpen = onOpen,
severities = listOf(null, "info", "warning", "high", "critical"), severities = listOf(null, "info", "warning", "high", "critical"),
hostnameFilter = hostname, hostnameFilter = hostname,
@@ -224,6 +254,9 @@ fun ReportsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
var refreshing by remember { mutableStateOf(false) } var refreshing by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH,
)
fun load(showRefreshIndicator: Boolean = false) { fun load(showRefreshIndicator: Boolean = false) {
scope.launch { scope.launch {
@@ -274,7 +307,7 @@ fun ReportsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
items(data?.items.orEmpty(), key = { it.id }) { item -> items(data?.items.orEmpty(), key = { it.id }) { item ->
ListRow( ListRow(
title = item.summary, title = item.summary,
subtitle = "${item.hostname} · ${item.occurredAt}", subtitle = "${item.hostname} · ${SacDateTimes.format(item.occurredAt, dateMode)}",
onClick = { onOpen(item.id) }, onClick = { onOpen(item.id) },
) )
} }
@@ -0,0 +1,46 @@
package ru.kalinamall.seaca.ui.util
import ru.kalinamall.seaca.data.DateTimeDisplayMode
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
object SacDateTimes {
fun format(raw: String?, mode: DateTimeDisplayMode): String {
if (raw.isNullOrBlank()) return ""
val zoned = parse(raw) ?: return raw.trim()
val local = zoned.withZoneSameInstant(ZoneId.systemDefault())
return when (mode) {
DateTimeDisplayMode.LOCALE -> {
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM)
.withLocale(Locale.getDefault())
.format(local)
}
else -> {
val pattern = mode.pattern ?: DateTimeDisplayMode.SLASH.pattern!!
DateTimeFormatter.ofPattern(pattern, Locale.getDefault()).format(local)
}
}
}
private fun parse(raw: String): ZonedDateTime? {
val trimmed = raw.trim()
val normalised = trimmed.replace(Regex(" ([+-]\\d{2}:\\d{2})$"), "$1")
val candidates = listOf(normalised, trimmed)
for (candidate in candidates) {
runCatching { return OffsetDateTime.parse(candidate).toZonedDateTime() }
runCatching { return ZonedDateTime.parse(candidate) }
runCatching { return Instant.parse(candidate).atZone(ZoneId.systemDefault()) }
runCatching {
return LocalDateTime.parse(candidate, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.atZone(ZoneId.systemDefault())
}
}
return null
}
}
+1
View File
@@ -8,4 +8,5 @@
<string name="channel_high_silent">SAC: важные (без звука)</string> <string name="channel_high_silent">SAC: важные (без звука)</string>
<string name="channel_critical_silent">SAC: критические (без звука)</string> <string name="channel_critical_silent">SAC: критические (без звука)</string>
<string name="notification_mode_title">Push-уведомления</string> <string name="notification_mode_title">Push-уведомления</string>
<string name="date_time_format_title">Формат даты и времени</string>
</resources> </resources>