From 970b12f1bd5f28325eeae191b8d48549be932976 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:18:32 +0200 Subject: [PATCH] Add Time Format setting (#4118) Analogous to the Date Format setting. Offers 4 modes: - "Follow system" - based on the phone's time settings (which usually offer 12h/24h as a setting that may be toggled regardless of system language defaults) - this can be changed with an app switch which is why some callers are keyed on the 24-hour state. - "Follow language" - uses the app language's default formats - e.g. English: 12-hour, German: 24-hour, etc. - "24-hour": Forces 24-hour display - "12-hour": Forces 12-hour display --- CHANGELOG.md | 1 + .../java/eu/kanade/domain/ui/UiPreferences.kt | 50 +++++++++++++- .../eu/kanade/domain/ui/model/TimeFormat.kt | 11 +++ .../presentation/components/DateText.kt | 4 +- .../presentation/history/HistoryScreen.kt | 17 +++++ .../history/components/HistoryItem.kt | 8 ++- .../screen/SettingsAppearanceScreen.kt | 26 +++++-- .../more/settings/screen/about/AboutScreen.kt | 17 ++++- .../settings/screen/debug/DebugInfoScreen.kt | 6 +- .../settings/screen/debug/WorkerInfoScreen.kt | 68 ++++++++++++++----- .../presentation/util/TwentyFourHourSystem.kt | 32 +++++++++ .../ui/manga/track/TrackInfoDialog.kt | 9 ++- .../tachiyomi/util/lang/DateExtensions.kt | 12 ++-- .../moko-resources/base/strings.xml | 7 ++ 14 files changed, 228 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/eu/kanade/domain/ui/model/TimeFormat.kt create mode 100644 app/src/main/java/eu/kanade/presentation/util/TwentyFourHourSystem.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c27a5fd..41cae794a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Show pages progressively while they download ([@wwww-wwww](https://github.com/wwww-wwww)) ([#4023](https://github.com/mihonapp/mihon/pull/4023)) - Lower memory use for animated images and support cropping borders in long strip ([@wwww-wwww](https://github.com/wwww-wwww)) ([#4029](https://github.com/mihonapp/mihon/pull/4029)) - Pair more wide pages in dual page view by comparing them with neighbouring pages ([@wwww-wwww](https://github.com/wwww-wwww)) ([#4051](https://github.com/mihonapp/mihon/pull/4051)) +- Add a Time Format setting to customize how times are displayed ([@MajorTanya](https://github.com/MajorTanya)) ([#4118](https://github.com/mihonapp/mihon/pull/4118)) ### Changed - Use a new decoder for images Android can't decode itself, like AVIF, JPEG XL and HEIF, and support JPEG 2000 images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3786](https://github.com/mihonapp/mihon/pull/3786)) diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index 354b1890f..a23715b97 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -1,17 +1,22 @@ package eu.kanade.domain.ui +import android.text.format.DateFormat import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.TabletUiMode import eu.kanade.domain.ui.model.ThemeMode +import eu.kanade.domain.ui.model.TimeFormat import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable +import logcat.LogPriority import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.getEnum +import tachiyomi.core.common.util.system.logcat import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder import java.time.format.FormatStyle import java.util.Locale @@ -38,14 +43,53 @@ class UiPreferences( val dateFormat: Preference = preferenceStore.getString("app_date_format", "") + val timeFormat: Preference = preferenceStore.getEnum("app_time_format", TimeFormat.FOLLOW_LANGUAGE) + val tabletUiMode: Preference = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) val imagesInDescription: Preference = preferenceStore.getBoolean("pref_render_images_description", true) companion object { - fun dateFormat(format: String): DateTimeFormatter = when (format) { - "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - else -> DateTimeFormatter.ofPattern(format, Locale.getDefault()) + fun dateFormat(format: String, locale: Locale): DateTimeFormatter = when (format) { + "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale) + else -> DateTimeFormatter.ofPattern(format, locale) + } + + fun timeFormat(timeFormat: TimeFormat, locale: Locale, is24HourSystem: Boolean): DateTimeFormatter { + val use24Hours = when (timeFormat) { + TimeFormat.FOLLOW_LANGUAGE -> { + return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale) + } + + TimeFormat.FOLLOW_SYSTEM -> is24HourSystem + TimeFormat.HOURS_24 -> true + TimeFormat.HOURS_12 -> false + } + + val skeleton = if (use24Hours) "Hm" else "hm" + val pattern = DateFormat.getBestDateTimePattern(locale, skeleton) + return try { + DateTimeFormatter.ofPattern(pattern, locale) + } catch (e: IllegalArgumentException) { + val fallbackPattern = if (use24Hours) "HH:mm" else "h:mm a" + logcat(LogPriority.ERROR, e) { + "Failed to apply best pattern ('$pattern'), falling back to '$fallbackPattern'" + } + DateTimeFormatter.ofPattern(fallbackPattern, locale) + } + } + + fun dateTimeFormat( + dateFormat: String, + timeFormat: TimeFormat, + locale: Locale, + is24HourSystem: Boolean, + ): DateTimeFormatter { + return DateTimeFormatterBuilder() + .append(dateFormat(dateFormat, locale)) + .appendLiteral(' ') + .append(timeFormat(timeFormat, locale, is24HourSystem)) + .toFormatter(locale) } } } diff --git a/app/src/main/java/eu/kanade/domain/ui/model/TimeFormat.kt b/app/src/main/java/eu/kanade/domain/ui/model/TimeFormat.kt new file mode 100644 index 000000000..6817cc938 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/ui/model/TimeFormat.kt @@ -0,0 +1,11 @@ +package eu.kanade.domain.ui.model + +import dev.icerock.moko.resources.StringResource +import tachiyomi.i18n.MR + +enum class TimeFormat(val titleRes: StringResource) { + FOLLOW_LANGUAGE(MR.strings.follow_language), + FOLLOW_SYSTEM(MR.strings.follow_system), + HOURS_24(MR.strings.time_24_hours), + HOURS_12(MR.strings.time_12_hours), +} diff --git a/app/src/main/java/eu/kanade/presentation/components/DateText.kt b/app/src/main/java/eu/kanade/presentation/components/DateText.kt index 2c16e5956..25067ad37 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DateText.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -3,6 +3,7 @@ package eu.kanade.presentation.components import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLocale import eu.kanade.domain.ui.UiPreferences import eu.kanade.tachiyomi.util.lang.toRelativeString import kotlinx.datetime.LocalDate @@ -27,10 +28,11 @@ fun relativeDateText( localDate: LocalDate?, ): String { val context = LocalContext.current + val locale = LocalLocale.current val preferences = remember { context.appGraph.uiPreferences } val relativeTime = remember { preferences.relativeTime.get() } - val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat.get()) } + val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat.get(), locale.platformLocale) } return localDate?.toRelativeString( context = context, diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index ee03975f9..dcc55b01d 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -6,9 +6,14 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter +import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarTitle @@ -16,8 +21,10 @@ import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.relativeDateText import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.theme.TachiyomiPreviewTheme +import eu.kanade.presentation.util.rememberIs24HourSystem import eu.kanade.tachiyomi.ui.history.HistoryViewModel import kotlinx.datetime.LocalDate +import mihon.app.di.appGraph import mihon.icons.materialsymbols.MaterialSymbols import mihon.icons.materialsymbols.rounded.DeleteSweep import tachiyomi.domain.history.model.HistoryWithRelations @@ -28,6 +35,7 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen +import tachiyomi.presentation.core.util.collectAsState @Composable fun HistoryScreen( @@ -99,6 +107,14 @@ private fun HistoryScreenContent( onClickDelete: (HistoryWithRelations) -> Unit, onClickFavorite: (HistoryWithRelations) -> Unit, ) { + val context = LocalContext.current + val locale = LocalLocale.current + val is24HourSystem = rememberIs24HourSystem() + val timeFormat by remember { context.appGraph.uiPreferences.timeFormat }.collectAsState() + val timeFormatter = remember(timeFormat, locale, is24HourSystem) { + UiPreferences.timeFormat(timeFormat, locale.platformLocale, is24HourSystem) + } + FastScrollLazyColumn( contentPadding = contentPadding, ) { @@ -124,6 +140,7 @@ private fun HistoryScreenContent( HistoryItem( modifier = Modifier.animateItem(), history = value, + timeFormatter = timeFormatter, onClickCover = { onClickCover(value) }, onClickResume = { onClickResume(value) }, onClickDelete = { onClickDelete(value) }, diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt index 6a08464cf..81bfa975d 100644 --- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt +++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt @@ -31,12 +31,15 @@ import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle private val HistoryItemHeight = 96.dp @Composable fun HistoryItem( history: HistoryWithRelations, + timeFormatter: DateTimeFormatter, onClickCover: () -> Unit, onClickResume: () -> Unit, onClickDelete: () -> Unit, @@ -68,7 +71,9 @@ fun HistoryItem( overflow = TextOverflow.Ellipsis, style = textStyle, ) - val readAt = remember { history.readAt?.toTimestampString() ?: "" } + val readAt = remember(timeFormatter, history.readAt) { + history.readAt?.toTimestampString(timeFormatter) ?: "" + } Text( text = if (history.chapterNumber > -1) { stringResource( @@ -114,6 +119,7 @@ private fun HistoryItemPreviews( Surface { HistoryItem( history = historyWithRelations, + timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT), onClickCover = {}, onClickResume = {}, onClickDelete = {}, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt index 7d2ba39ca..028202e9d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt @@ -7,17 +7,20 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLocale import androidx.core.app.ActivityCompat import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.model.TabletUiMode import eu.kanade.domain.ui.model.ThemeMode +import eu.kanade.domain.ui.model.TimeFormat import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.screen.appearance.AppLanguageScreen import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget +import eu.kanade.presentation.util.rememberIs24HourSystem import eu.kanade.tachiyomi.util.system.toast import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDateTime @@ -101,12 +104,14 @@ object SettingsAppearanceScreen : SearchableSettings { ): Preference.PreferenceGroup { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow + val locale = LocalLocale.current + val is24HourSystem = rememberIs24HourSystem() val now = remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() } val dateFormat by uiPreferences.dateFormat.collectAsState() - val formattedNow = remember(dateFormat) { - UiPreferences.dateFormat(dateFormat).format(now) + val formattedDateNow = remember(dateFormat) { + UiPreferences.dateFormat(dateFormat, locale.platformLocale).format(now) } return Preference.PreferenceGroup( @@ -130,18 +135,31 @@ object SettingsAppearanceScreen : SearchableSettings { preference = uiPreferences.dateFormat, entries = DateFormats .associateWith { - val formattedDate = UiPreferences.dateFormat(it).format(now) + val formattedDate = UiPreferences.dateFormat(it, locale.platformLocale).format(now) "${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)" }, title = stringResource(MR.strings.pref_date_format), ), + Preference.PreferenceItem.ListPreference( + preference = uiPreferences.timeFormat, + entries = TimeFormat.entries + .associateWith { + val formattedTime = UiPreferences.timeFormat( + it, + locale.platformLocale, + is24HourSystem, + ).format(now) + "${stringResource(it.titleRes)} ($formattedTime)" + }, + title = stringResource(MR.strings.pref_time_format), + ), Preference.PreferenceItem.SwitchPreference( preference = uiPreferences.relativeTime, title = stringResource(MR.strings.pref_relative_format), subtitle = stringResource( MR.strings.pref_relative_format_summary, stringResource(MR.strings.relative_time_today), - formattedNow, + formattedDateNow, ), ), Preference.PreferenceItem.SwitchPreference( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt index 758c2890d..eb5db654b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt @@ -1,6 +1,7 @@ package eu.kanade.presentation.more.settings.screen.about import android.content.Context +import android.text.format.DateFormat import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -27,6 +28,7 @@ import eu.kanade.presentation.more.LogoHeader import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.util.LocalBackPress import eu.kanade.presentation.util.Screen +import eu.kanade.presentation.util.rememberIs24HourSystem import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.updater.RELEASE_URL import eu.kanade.tachiyomi.ui.more.NewUpdateScreen @@ -61,6 +63,7 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.Locale import kotlin.time.Instant object AboutScreen : Screen() { @@ -74,6 +77,7 @@ object AboutScreen : Screen() { val navigator = LocalNavigator.currentOrThrow var isCheckingUpdates by remember { mutableStateOf(false) } val crashLogUtil = remember { context.appGraph.crashLogUtil } + val is24HourSystem = rememberIs24HourSystem() Scaffold( topBar = { scrollBehavior -> @@ -96,7 +100,9 @@ object AboutScreen : Screen() { item { TextPreferenceWidget( title = stringResource(MR.strings.version), - subtitle = getVersionName(withBuildDate = true), + // Keyed on the system 12/24h setting, which can change while the app is backgrounded. + // Date/time prefs only change in Appearance, which recreates this screen on return. + subtitle = remember(is24HourSystem) { getVersionName(withBuildDate = true) }, onPreferenceClick = { val deviceInfo = crashLogUtil.getDebugInfo() context.copyToClipboard("Debug information", deviceInfo) @@ -275,11 +281,16 @@ object AboutScreen : Screen() { internal fun getFormattedBuildTime(): String { return try { + val context = Injekt.get() + val uiPreferences = context.appGraph.uiPreferences Instant.parse(BuildConfig.BUILD_TIME) .toLocalDateTime(TimeZone.currentSystemDefault()) .toDateTimestampString( - UiPreferences.dateFormat( - Injekt.get().appGraph.uiPreferences.dateFormat.get(), + UiPreferences.dateTimeFormat( + uiPreferences.dateFormat.get(), + uiPreferences.timeFormat.get(), + Locale.getDefault(), + DateFormat.is24HourFormat(context), ), ) } catch (_: Exception) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt index 1d89c9ec6..e90d7da4e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt @@ -18,6 +18,7 @@ import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.PreferenceScaffold import eu.kanade.presentation.more.settings.screen.about.AboutScreen import eu.kanade.presentation.util.Screen +import eu.kanade.presentation.util.rememberIs24HourSystem import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.copyToClipboard @@ -59,6 +60,7 @@ class DebugInfoScreen : Screen() { private fun getAppInfoGroup(): Preference.PreferenceGroup { val context = LocalContext.current val scope = rememberCoroutineScope() + val is24HourSystem = rememberIs24HourSystem() val installationIdPref = remember { context.appGraph.basePreferences.installationId } val installationId by installationIdPref.collectAsState() @@ -72,7 +74,9 @@ class DebugInfoScreen : Screen() { ), Preference.PreferenceItem.TextPreference( title = "Build time", - subtitle = AboutScreen.getFormattedBuildTime(), + // Keyed on the system 12/24h setting, which can change while the app is backgrounded. + // Date/time prefs only change in Appearance, which recreates this screen on return. + subtitle = remember(is24HourSystem) { AboutScreen.getFormattedBuildTime() }, ), Preference.PreferenceItem.TextPreference( title = "Installation ID", diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt index eb3d8298b..e4ac1090c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt @@ -1,6 +1,7 @@ package eu.kanade.presentation.more.settings.screen.debug import android.content.Context +import android.text.format.DateFormat import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding @@ -9,6 +10,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -31,11 +33,13 @@ import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.util.Screen +import eu.kanade.presentation.util.rememberIs24HourSystem import eu.kanade.tachiyomi.util.lang.toDateTimestampString import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.workManager +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @@ -46,6 +50,8 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.plus +import java.time.format.DateTimeFormatter +import java.util.Locale import kotlin.time.Instant class WorkerInfoScreen : Screen() { @@ -60,6 +66,12 @@ class WorkerInfoScreen : Screen() { val navigator = LocalNavigator.currentOrThrow val viewModel = metroViewModel() + + val is24HourSystem = rememberIs24HourSystem() + // Keyed on the system 12/24h setting, which can change while the app is backgrounded. + // Date/time prefs only change in Appearance, which recreates this screen on return. + LaunchedEffect(is24HourSystem) { viewModel.onSystemTimeFormatChanged(is24HourSystem) } + val enqueued by viewModel.enqueued.collectAsState() val finished by viewModel.finished.collectAsState() val running by viewModel.running.collectAsState() @@ -124,29 +136,53 @@ class WorkerInfoScreen : Screen() { @ViewModelKey @ContributesIntoMap(AppScope::class) class WorkerInfoViewModel( - private val context: Context, + context: Context, ) : ViewModel() { private val workManager = context.workManager + private val dateFormatPref = context.appGraph.uiPreferences.dateFormat + private val timeFormatPref = context.appGraph.uiPreferences.timeFormat + private val is24HourSystem = MutableStateFlow(DateFormat.is24HourFormat(context)) - val finished = workManager - .getWorkInfosFlow( + private val dateTimeFormatter = combine( + dateFormatPref.changes(), + timeFormatPref.changes(), + is24HourSystem, + ) { date, time, is24 -> + UiPreferences.dateTimeFormat(date, time, Locale.getDefault(), is24) + } + + fun onSystemTimeFormatChanged(value: Boolean) { + is24HourSystem.value = value + } + + val finished = combine( + workManager.getWorkInfosFlow( WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED), - ) - .map(::constructString) + ), + dateTimeFormatter, + ::constructString, + ) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") - val running = workManager - .getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING)) - .map(::constructString) + val running = combine( + workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING)), + dateTimeFormatter, + ::constructString, + ) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") - val enqueued = workManager - .getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED)) - .map(::constructString) + val enqueued = combine( + workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED)), + dateTimeFormatter, + ::constructString, + ) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") - private fun constructString(list: List) = buildString { + private fun constructString( + list: List, + dateTimeFormatter: DateTimeFormatter, + ) = buildString { if (list.isEmpty()) { appendLine("-") } else { @@ -160,11 +196,7 @@ class WorkerInfoScreen : Screen() { if (workInfo.state == WorkInfo.State.ENQUEUED) { val timestamp = Instant.fromEpochMilliseconds(workInfo.nextScheduleTimeMillis) .toLocalDateTime(TimeZone.currentSystemDefault()) - .toDateTimestampString( - UiPreferences.dateFormat( - context.appGraph.uiPreferences.dateFormat.get(), - ), - ) + .toDateTimestampString(dateTimeFormatter) appendLine("Next scheduled run: $timestamp") appendLine("Attempt #${workInfo.runAttemptCount + 1}") } diff --git a/app/src/main/java/eu/kanade/presentation/util/TwentyFourHourSystem.kt b/app/src/main/java/eu/kanade/presentation/util/TwentyFourHourSystem.kt new file mode 100644 index 000000000..cf58f2eab --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/util/TwentyFourHourSystem.kt @@ -0,0 +1,32 @@ +package eu.kanade.presentation.util + +import android.text.format.DateFormat +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner + +@Composable +fun rememberIs24HourSystem(): Boolean { + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + var is24 by remember { mutableStateOf(DateFormat.is24HourFormat(context)) } + DisposableEffect(lifecycleOwner.lifecycle) { + val observer = object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + is24 = DateFormat.is24HourFormat(context) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + return is24 +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index 73f527714..4f1fca509 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -104,9 +105,15 @@ data class TrackInfoDialogHomeScreen( override fun Content() { val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current + val locale = LocalLocale.current val viewModel = assistedMetroViewModel { create(mangaId = mangaId, sourceId = sourceId) } - val dateFormat = remember { UiPreferences.dateFormat(context.appGraph.uiPreferences.dateFormat.get()) } + val dateFormat = remember { + UiPreferences.dateFormat( + context.appGraph.uiPreferences.dateFormat.get(), + locale.platformLocale, + ) + } val state by viewModel.state.collectAsState() TrackInfoDialogHome( diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt index 8d037d5ed..35425cfac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt @@ -13,21 +13,17 @@ import tachiyomi.core.common.i18n.pluralStringResource import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import kotlin.math.absoluteValue import kotlin.time.Clock import kotlin.time.Instant fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): String { val javaLocalDateTime = this.toJavaLocalDateTime() - val date = dateTimeFormatter.format(javaLocalDateTime) - val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(javaLocalDateTime) - return "$date $time" + return dateTimeFormatter.format(javaLocalDateTime) } -fun Instant.toTimestampString(): String { - return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - .format(toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) +fun Instant.toTimestampString(timeFormatter: DateTimeFormatter): String { + return timeFormatter.format(toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) } fun Long.convertEpochMillisZone( @@ -55,7 +51,7 @@ fun Instant.toJavaLocalDate(): java.time.LocalDate { fun LocalDate.toRelativeString( context: Context, relative: Boolean = true, - dateFormat: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT), + dateFormat: DateTimeFormatter, ): String { if (!relative) { return dateFormat.format(this.toJavaLocalDate()) diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index d54c7a7f0..4474f5c77 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -250,6 +250,13 @@ \"%1$s\" instead of \"%2$s\" Date format + Time format + + Follow system + + Follow language + 24-hour + 12-hour Render images in manga descriptions Manage notifications