瀏覽代碼

Bring back simplified relative timestamp setting

Except now it's just an on/off toggle for relative up to a week.
arkon 1 年之前
父節點
當前提交
56d2464870

+ 1 - 1
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()}\"")

+ 2 - 0
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)

+ 9 - 1
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,
+            )
         },
     )
 }

+ 3 - 1
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,
                     )
                 }

+ 14 - 3
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<ChapterItem>,
+    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 }

+ 14 - 0
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,
+                    ),
+                ),
             ),
         )
     }

+ 2 - 1
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,

+ 6 - 0
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
         }
 

+ 1 - 0
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(),

+ 1 - 0
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)
 

+ 9 - 2
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<UpdatesScreenModel.State>(State()) {
 
     private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
     val events: Flow<Event> = _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<Int> = arrayOf(-1, -1)
@@ -373,7 +376,7 @@ class UpdatesScreenModel(
         val selected = items.filter { it.selected }
         val selectionMode = selected.isNotEmpty()
 
-        fun getUiModel(context: Context): List<UpdatesUiModel> {
+        fun getUiModel(context: Context, relativeTime: Boolean): List<UpdatesUiModel> {
             val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().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.

+ 1 - 0
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,

+ 40 - 0
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)
+            [email protected] + timeZone.rawOffset + dstOffset
+        }
+    }
+
+fun Long.floorNearest(to: Long): Long {
+    return this.floorDiv(to) * to
+}

+ 9 - 0
i18n/src/main/res/values/strings.xml

@@ -201,6 +201,9 @@
     <string name="theme_yotsuba">Yotsuba</string>
     <string name="theme_tidalwave">Tidal Wave</string>
     <string name="pref_dark_theme_pure_black">Pure black dark mode</string>
+    <string name="pref_relative_format">Relative timestamps</string>
+    <!-- "Today" instead of "2023-12-31" -->
+    <string name="pref_relative_format_summary">\"%1$s\" instead of \"%2$s\"</string>
     <string name="pref_date_format">Date format</string>
 
     <string name="pref_manage_notifications">Manage notifications</string>
@@ -223,6 +226,12 @@
     <string name="pref_show_nsfw_source">Show in sources and extensions lists</string>
     <string name="parental_controls_info">This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing NSFW (18+) content within the app.</string>
 
+    <string name="relative_time_today">Today</string>
+    <plurals name="relative_time">
+        <item quantity="one">Yesterday</item>
+        <item quantity="other">%1$d days ago</item>
+    </plurals>
+
       <!-- Library section -->
     <string name="pref_category_display">Display</string>
     <string name="pref_library_columns">Grid size</string>

+ 0 - 1
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 {