diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e0658c1d8c..93d48d8d4c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 105 + versionCode = 106 versionName = "0.14.6" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") 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 ac04a51e0e..294812bdce 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -28,6 +28,8 @@ class UiPreferences( fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) + fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true) + fun dateFormat() = preferenceStore.getString("app_date_format", "") fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) diff --git a/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt b/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt index 598bad83e5..2466e2eedf 100644 --- a/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt @@ -3,6 +3,8 @@ package eu.kanade.presentation.components import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import eu.kanade.tachiyomi.util.lang.toRelativeString import tachiyomi.presentation.core.components.ListGroupHeader import java.text.DateFormat import java.util.Date @@ -11,12 +13,18 @@ import java.util.Date fun RelativeDateHeader( modifier: Modifier = Modifier, date: Date, + relativeTime: Boolean, dateFormat: DateFormat, ) { + val context = LocalContext.current ListGroupHeader( modifier = modifier, text = remember { - dateFormat.format(date) + date.toRelativeString( + context, + relativeTime, + dateFormat, + ) }, ) } 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 3bac670d0e..fc755c9225 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -98,7 +98,8 @@ private fun HistoryScreenContent( onClickDelete: (HistoryWithRelations) -> Unit, preferences: UiPreferences = Injekt.get(), ) { - val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } + val relativeTime = remember { preferences.relativeTime().get() } + val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } FastScrollLazyColumn( contentPadding = contentPadding, @@ -118,6 +119,7 @@ private fun HistoryScreenContent( RelativeDateHeader( modifier = Modifier.animateItemPlacement(), date = item.date, + relativeTime = relativeTime, dateFormat = dateFormat, ) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 352e6aedb5..65963cef34 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -63,6 +63,7 @@ import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.ui.manga.ChapterItem import eu.kanade.tachiyomi.ui.manga.MangaScreenModel +import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.system.copyToClipboard import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.service.missingChaptersCount @@ -84,6 +85,7 @@ fun MangaScreen( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, fetchInterval: Int?, + dateRelativeTime: Boolean, dateFormat: DateFormat, isTabletUi: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, @@ -139,6 +141,7 @@ fun MangaScreen( MangaScreenSmallImpl( state = state, snackbarHostState = snackbarHostState, + dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, fetchInterval = fetchInterval, chapterSwipeStartAction = chapterSwipeStartAction, @@ -175,6 +178,7 @@ fun MangaScreen( MangaScreenLargeImpl( state = state, snackbarHostState = snackbarHostState, + dateRelativeTime = dateRelativeTime, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, dateFormat = dateFormat, @@ -214,6 +218,7 @@ fun MangaScreen( private fun MangaScreenSmallImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, + dateRelativeTime: Boolean, dateFormat: DateFormat, fetchInterval: Int?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, @@ -282,11 +287,9 @@ private fun MangaScreenSmallImpl( } val animatedTitleAlpha by animateFloatAsState( if (firstVisibleItemIndex > 0) 1f else 0f, - label = "titleAlpha", ) val animatedBgAlpha by animateFloatAsState( if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f, - label = "bgAlpha", ) MangaToolbar( title = state.manga.title, @@ -427,6 +430,7 @@ private fun MangaScreenSmallImpl( sharedChapterItems( manga = state.manga, chapters = chapters, + dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, @@ -445,6 +449,7 @@ private fun MangaScreenSmallImpl( fun MangaScreenLargeImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, + dateRelativeTime: Boolean, dateFormat: DateFormat, fetchInterval: Int?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, @@ -650,6 +655,7 @@ fun MangaScreenLargeImpl( sharedChapterItems( manga = state.manga, chapters = chapters, + dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, @@ -711,6 +717,7 @@ private fun SharedMangaBottomActionMenu( private fun LazyListScope.sharedChapterItems( manga: Manga, chapters: List, + dateRelativeTime: Boolean, dateFormat: DateFormat, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, @@ -739,7 +746,11 @@ private fun LazyListScope.sharedChapterItems( date = chapterItem.chapter.dateUpload .takeIf { it > 0L } ?.let { - dateFormat.format(Date(it)) + Date(it).toRelativeString( + context, + dateRelativeTime, + dateFormat, + ) }, readProgress = chapterItem.chapter.lastPageRead .takeIf { !chapterItem.chapter.read && it > 0L } 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 d98198155d..4540aee95d 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 @@ -123,6 +123,11 @@ object SettingsAppearanceScreen : SearchableSettings { var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") } val now = remember { Date().time } + val dateFormat by uiPreferences.dateFormat().collectAsState() + val formattedNow = remember(dateFormat) { + UiPreferences.dateFormat(dateFormat).format(now) + } + LaunchedEffect(currentLanguage) { val locale = if (currentLanguage.isEmpty()) { LocaleListCompat.getEmptyLocaleList() @@ -162,6 +167,15 @@ object SettingsAppearanceScreen : SearchableSettings { "${it.ifEmpty { stringResource(R.string.label_default) }} ($formattedDate)" }, ), + Preference.PreferenceItem.SwitchPreference( + pref = uiPreferences.relativeTime(), + title = stringResource(R.string.pref_relative_format), + subtitle = stringResource( + R.string.pref_relative_format_summary, + stringResource(R.string.relative_time_today), + formattedNow, + ), + ), ), ) } diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index 1f4a56d5f1..1572faff4f 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -43,6 +43,7 @@ fun UpdateScreen( state: UpdatesScreenModel.State, snackbarHostState: SnackbarHostState, lastUpdated: Long, + relativeTime: Boolean, onClickCover: (UpdatesItem) -> Unit, onSelectAll: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -113,7 +114,7 @@ fun UpdateScreen( } updatesUiItems( - uiModels = state.getUiModel(context), + uiModels = state.getUiModel(context, relativeTime), selectionMode = state.selectionMode, onUpdateSelected = onUpdateSelected, onClickCover = onClickCover, diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 75edfdd462..3c15c913d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -375,6 +375,12 @@ object Migrations { pref.getAndSet { it - "battery_not_low" } } } + if (oldVersion < 106) { + val pref = preferenceStore.getInt("relative_time", 7) + if (pref.get() == 0) { + uiPreferences.relativeTime().set(false) + } + } return true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 8f56c4fcb9..39595fbe5a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -99,6 +99,7 @@ class MangaScreen( MangaScreen( state = successState, snackbarHostState = screenModel.snackbarHostState, + dateRelativeTime = screenModel.relativeTime, dateFormat = screenModel.dateFormat, fetchInterval = successState.manga.fetchInterval, isTabletUi = isTabletUi(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 71b7c4bf75..0827caded2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -125,6 +125,7 @@ class MangaScreenModel( val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get() val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get() + val relativeTime by uiPreferences.relativeTime().asState(coroutineScope) val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 6e0c1c4742..62ac21cc15 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.util.lang.toDateKey +import eu.kanade.tachiyomi.util.lang.toRelativeString import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -58,12 +59,14 @@ class UpdatesScreenModel( private val getChapter: GetChapter = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), + uiPreferences: UiPreferences = Injekt.get(), ) : StateScreenModel(State()) { private val _events: Channel = Channel(Int.MAX_VALUE) val events: Flow = _events.receiveAsFlow() val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope) + val relativeTime by uiPreferences.relativeTime().asState(coroutineScope) // First and last selected index in list private val selectedPositions: Array = arrayOf(-1, -1) @@ -373,7 +376,7 @@ class UpdatesScreenModel( val selected = items.filter { it.selected } val selectionMode = selected.isNotEmpty() - fun getUiModel(context: Context): List { + fun getUiModel(context: Context, relativeTime: Boolean): List { val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get().dateFormat().get())) return items @@ -383,7 +386,11 @@ class UpdatesScreenModel( val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0) when { beforeDate.time != afterDate.time && afterDate.time != 0L -> { - val text = dateFormat.format(afterDate) + val text = afterDate.toRelativeString( + context = context, + relative = relativeTime, + dateFormat = dateFormat, + ) UpdatesUiModel.Header(text) } // Return null to avoid adding a separator between two items. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index cf33f69c42..78a44dd23a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -57,6 +57,7 @@ object UpdatesTab : Tab { state = state, snackbarHostState = screenModel.snackbarHostState, lastUpdated = screenModel.lastUpdated, + relativeTime = screenModel.relativeTime, onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) }, onSelectAll = screenModel::toggleAllSelection, onInvertSelection = screenModel::invertSelection, 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 88477e8d57..e2f6836853 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 @@ -1,5 +1,7 @@ package eu.kanade.tachiyomi.util.lang +import android.content.Context +import eu.kanade.tachiyomi.R import java.text.DateFormat import java.time.Instant import java.time.LocalDateTime @@ -42,3 +44,41 @@ fun Long.toDateKey(): Date { cal[Calendar.MILLISECOND] = 0 return cal.time } + +private const val MILLISECONDS_IN_DAY = 86_400_000L + +fun Date.toRelativeString( + context: Context, + relative: Boolean = true, + dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT), +): String { + if (!relative) { + return dateFormat.format(this) + } + val now = Date() + val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) + val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt() + return when { + difference < 0 -> dateFormat.format(this) + difference < MILLISECONDS_IN_DAY -> context.getString(R.string.relative_time_today) + difference < MILLISECONDS_IN_DAY.times(7) -> context.resources.getQuantityString( + R.plurals.relative_time, + days, + days, + ) + else -> dateFormat.format(this) + } +} + +private val Date.timeWithOffset: Long + get() { + return Calendar.getInstance().run { + time = this@timeWithOffset + val dstOffset = get(Calendar.DST_OFFSET) + this@timeWithOffset.time + timeZone.rawOffset + dstOffset + } + } + +fun Long.floorNearest(to: Long): Long { + return this.floorDiv(to) * to +} diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 67f33acca5..bed8f08362 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -201,6 +201,9 @@ Yotsuba Tidal Wave Pure black dark mode + Relative timestamps + + \"%1$s\" instead of \"%2$s\" Date format Manage notifications @@ -223,6 +226,12 @@ Show in sources and extensions lists This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing NSFW (18+) content within the app. + Today + + Yesterday + %1$d days ago + + Display Grid size diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt index eea05b4cab..6720e74c58 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt @@ -70,7 +70,6 @@ fun AdaptiveSheet( val alpha by animateFloatAsState( targetValue = targetAlpha, animationSpec = sheetAnimationSpec, - label = "alpha", ) val internalOnDismissRequest: () -> Unit = { scope.launch {