diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt index 6b12e3739..f514d0a7c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt @@ -53,7 +53,6 @@ import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.Crash import com.hippo.ehviewer.util.FavouriteStatusRouter import com.hippo.ehviewer.util.FileUtils -import com.hippo.ehviewer.util.ReadableTime import com.hippo.ehviewer.util.isAtLeastP import com.hippo.ehviewer.util.isAtLeastQ import com.hippo.ehviewer.util.isAtLeastS @@ -105,7 +104,6 @@ class EhApplication : Application(), SingletonImageLoader.Factory { } super.onCreate() System.loadLibrary("ehviewer") - ReadableTime.initialize(this) lifecycleScope.launchIO { launch { EhTagDatabase diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt index 949e401ad..bf69e03ef 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt @@ -38,15 +38,15 @@ import com.hippo.ehviewer.client.exception.ParseException import com.hippo.ehviewer.client.exception.PiningException import com.hippo.ehviewer.client.getThumbKey import com.hippo.ehviewer.util.ExceptionUtils +import com.hippo.ehviewer.util.toEpochMillis import com.hippo.ehviewer.util.toFloatOrDefault import com.hippo.ehviewer.util.toIntOrDefault import com.hippo.ehviewer.util.trimAnd import com.hippo.ehviewer.util.unescapeXml import eu.kanade.tachiyomi.util.system.logcat -import java.time.Instant -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter -import java.util.Locale +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.format.MonthNames +import kotlinx.datetime.format.char import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.jsoup.nodes.Element @@ -78,8 +78,19 @@ object GalleryDetailParser { private val PATTERN_FAVORITE_SLOT = Regex("/fav.png\\); background-position:0px -(\\d+)px") private val EMPTY_GALLERY_COMMENT_LIST = GalleryCommentList(emptyList(), false) - private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter - .ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC) + + // dd MMMM yyyy, HH:mm + private val WEB_COMMENT_DATE_FORMAT = LocalDateTime.Format { + dayOfMonth() + char(' ') + monthName(MonthNames.ENGLISH_FULL) + char(' ') + year() + chars(", ") + hour() + char(':') + minute() + } private const val OFFENSIVE_STRING = "
(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)
" private const val PINING_STRING = "This gallery is pining for the fjords.
" @@ -378,7 +389,7 @@ object GalleryDetailParser { } else { temp.substring("Posted on ".length) to null } - val time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(timeStr)).toEpochMilli() + val time = WEB_COMMENT_DATE_FORMAT.parse(timeStr).toEpochMillis() // comment val c6 = element.getElementsByClass("c6").first() // fix underline support @@ -397,7 +408,7 @@ object GalleryDetailParser { } // last edited val c8 = element.getElementsByClass("c8").first() - val lastEdited = c8?.children()?.first()?.run { Instant.from(WEB_COMMENT_DATE_FORMAT.parse(text())).toEpochMilli() } ?: 0 + val lastEdited = c8?.children()?.first()?.run { WEB_COMMENT_DATE_FORMAT.parse(text()).toEpochMillis() } ?: 0 GalleryComment( id, score, editable, voteUpAble, voteUpEd, voteDownAble, voteDownEd, uploader, voteState, time, user, commentHtml, lastEdited, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/ParserUtils.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/ParserUtils.kt index b2901689c..3a1483b48 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/ParserUtils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/ParserUtils.kt @@ -16,20 +16,29 @@ package com.hippo.ehviewer.client.parser import com.hippo.ehviewer.util.toIntOrDefault +import com.hippo.ehviewer.util.toLocalDateTime import com.hippo.ehviewer.util.toLongOrDefault import com.hippo.ehviewer.util.unescapeXml -import java.time.Instant -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter -import java.util.Locale +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.format.char object ParserUtils { - private val formatter = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm", Locale.US).withZone(ZoneOffset.UTC) + // yyyy-MM-dd HH:mm + private val formatter = LocalDateTime.Format { + year() + char('-') + monthNumber() + char('-') + dayOfMonth() + char(' ') + hour() + char(':') + minute() + } @Synchronized fun formatDate(time: Long): String { - return formatter.format(Instant.ofEpochMilli(time)) + return formatter.format(time.toLocalDateTime()) } fun trim(str: String?): String { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt index 0bd146bab..7c15b9025 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt @@ -62,6 +62,7 @@ import com.hippo.ehviewer.util.isAtLeastT import com.hippo.ehviewer.util.mapToLongArray import com.hippo.ehviewer.util.requestPermission import com.hippo.ehviewer.util.toEpochMillis +import com.hippo.ehviewer.util.toLocalDateTime import com.hippo.unifile.UniFile import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.ui.reader.ReaderActivity @@ -74,7 +75,6 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.minus -import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.todayIn import moe.tarsin.coroutines.runSuspendCatching import splitties.init.appCtx @@ -439,8 +439,6 @@ suspend fun DialogState.showDatePicker(): String? { } }, ) - val date = dateMillis?.let { - kotlinx.datetime.Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.UTC).date.toString() - } + val date = dateMillis?.run { toLocalDateTime().date.toString() } return date } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/DateTimeUtil.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/DateTimeUtil.kt index 227fcc3f5..7fd241680 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/DateTimeUtil.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/DateTimeUtil.kt @@ -1,8 +1,18 @@ package com.hippo.ehviewer.util +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime -fun LocalDate.toEpochMillis(): Long = - atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds() +fun LocalDate.toEpochMillis(timeZone: TimeZone = TimeZone.UTC): Long = + atStartOfDayIn(timeZone).toEpochMilliseconds() + +fun LocalDateTime.toEpochMillis(timeZone: TimeZone = TimeZone.UTC): Long = + toInstant(timeZone).toEpochMilliseconds() + +fun Long.toLocalDateTime(timeZone: TimeZone = TimeZone.UTC): LocalDateTime = + Instant.fromEpochMilliseconds(this).toLocalDateTime(timeZone) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/ReadableTime.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/ReadableTime.kt index 4dcaba0f9..3c024f86f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/util/ReadableTime.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/ReadableTime.kt @@ -15,17 +15,20 @@ */ package com.hippo.ehviewer.util -import android.content.Context -import android.content.res.Resources import com.hippo.ehviewer.R -import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter import java.util.Locale +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format.MonthNames +import kotlinx.datetime.format.Padding +import kotlinx.datetime.format.char +import kotlinx.datetime.toLocalDateTime +import splitties.init.appCtx object ReadableTime { - private const val SECOND_MILLIS: Long = 1000 + private const val SECOND_MILLIS = 1000L private const val MINUTE_MILLIS = 60 * SECOND_MILLIS private const val HOUR_MILLIS = 60 * MINUTE_MILLIS private const val DAY_MILLIS = 24 * HOUR_MILLIS @@ -47,61 +50,101 @@ object ReadableTime { R.plurals.second, ) private val sCalendarLock = Any() - private val DATE_FORMAT_WITHOUT_YEAR = DateTimeFormatter.ofPattern("MMM d") - private val DATE_FORMAT_WITH_YEAR = DateTimeFormatter.ofPattern("MMM d, yyyy") - private val DATE_FORMAT_WITHOUT_YEAR_ZH = DateTimeFormatter.ofPattern("M月d日") - private val DATE_FORMAT_WITH_YEAR_ZH = DateTimeFormatter.ofPattern("yyyy年M月d日") - private val FILENAMABLE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss-SSS") - private val sDateFormatLock = Any() - private var sResources: Resources? = null - fun initialize(context: Context) { - sResources = context.applicationContext.resources + private val DATE_FORMAT_WITHOUT_YEAR = LocalDate.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED) + char(' ') + dayOfMonth(Padding.NONE) + } + private val DATE_FORMAT_WITH_YEAR = LocalDate.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED) + char(' ') + dayOfMonth(Padding.NONE) + chars(", ") + year() + } + private val DATE_FORMAT_WITHOUT_YEAR_ZH = LocalDate.Format { + monthNumber(Padding.NONE) + char('月') + dayOfMonth(Padding.NONE) + char('日') + } + private val DATE_FORMAT_WITH_YEAR_ZH = LocalDate.Format { + year() + char('年') + monthNumber(Padding.NONE) + char('月') + dayOfMonth(Padding.NONE) + char('日') } + // yyyy-MM-dd-HH-mm-ss-SSS + private val FILENAMABLE_DATE_FORMAT = LocalDateTime.Format { + year() + char('-') + monthNumber() + char('-') + dayOfMonth() + char('-') + hour() + char('-') + minute() + char('-') + second() + char('-') + secondFraction(3) + } + private val sDateFormatLock = Any() + private val resources = appCtx.resources + fun getTimeAgo(time: Long): String { - val resources = sResources - val now = System.currentTimeMillis() - if (time > now + 2 * MINUTE_MILLIS || time <= 0) { - return resources!!.getString(R.string.from_the_future) - } + val nowInstant = Clock.System.now() + val now = nowInstant.toEpochMilliseconds() val diff = now - time - if (diff < MINUTE_MILLIS) { - return resources!!.getString(R.string.just_now) - } else if (diff < 2 * MINUTE_MILLIS) { - return resources!!.getQuantityString(R.plurals.some_minutes_ago, 1, 1) - } else if (diff < 50 * MINUTE_MILLIS) { - val minutes = (diff / MINUTE_MILLIS).toInt() - return resources!!.getQuantityString(R.plurals.some_minutes_ago, minutes, minutes) - } else if (diff < 90 * MINUTE_MILLIS) { - return resources!!.getQuantityString(R.plurals.some_hours_ago, 1, 1) - } else if (diff < 24 * HOUR_MILLIS) { - val hours = (diff / HOUR_MILLIS).toInt() - return resources!!.getQuantityString(R.plurals.some_hours_ago, hours, hours) - } else if (diff < 48 * HOUR_MILLIS) { - return resources!!.getString(R.string.yesterday) - } else if (diff < WEEK_MILLIS) { - val days = (diff / DAY_MILLIS).toInt() - return resources!!.getString(R.string.some_days_ago, days) - } else { - synchronized(sCalendarLock) { - val nowDate = Instant.ofEpochMilli(now).atOffset(ZoneOffset.UTC) - val timeDate = Instant.ofEpochMilli(time).atOffset(ZoneOffset.UTC) + return when { + (diff < 0 || time <= 0) -> { + resources.getString(R.string.from_the_future) + } + diff < MINUTE_MILLIS -> { + resources.getString(R.string.just_now) + } + diff < 2 * MINUTE_MILLIS -> { + resources.getQuantityString(R.plurals.some_minutes_ago, 1, 1) + } + diff < 50 * MINUTE_MILLIS -> { + val minutes = (diff / MINUTE_MILLIS).toInt() + resources.getQuantityString(R.plurals.some_minutes_ago, minutes, minutes) + } + diff < 90 * MINUTE_MILLIS -> { + resources.getQuantityString(R.plurals.some_hours_ago, 1, 1) + } + diff < 24 * HOUR_MILLIS -> { + val hours = (diff / HOUR_MILLIS).toInt() + resources.getQuantityString(R.plurals.some_hours_ago, hours, hours) + } + diff < 48 * HOUR_MILLIS -> { + resources.getString(R.string.yesterday) + } + diff < WEEK_MILLIS -> { + val days = (diff / DAY_MILLIS).toInt() + resources.getString(R.string.some_days_ago, days) + } + else -> synchronized(sCalendarLock) { + val timeZone = TimeZone.currentSystemDefault() + val nowDate = nowInstant.toLocalDateTime(timeZone).date + val timeDate = time.toLocalDateTime(timeZone).date val nowYear = nowDate.year val timeYear = timeDate.year val isZh = Locale.getDefault().language == "zh" - return if (nowYear == timeYear) { - (if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR) - .format(timeDate.toInstant().atOffset(ZoneOffset.UTC)) + if (nowYear == timeYear) { + if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR } else { - (if (isZh) DATE_FORMAT_WITH_YEAR_ZH else DATE_FORMAT_WITH_YEAR) - .format(timeDate.toInstant().atOffset(ZoneOffset.UTC)) - } + if (isZh) DATE_FORMAT_WITH_YEAR_ZH else DATE_FORMAT_WITH_YEAR + }.format(timeDate) } } } fun getShortTimeInterval(time: Long): String { - val resources = sResources return buildString { for (i in 0 until SIZE) { val multiple = MULTIPLES[i] @@ -109,7 +152,7 @@ object ReadableTime { if (time > multiple * 1.5 || i == SIZE - 1) { append(quotient) .append(" ") - .append(resources!!.getQuantityString(UNITS[i], quotient.toInt())) + .append(resources.getQuantityString(UNITS[i], quotient.toInt())) break } } @@ -119,9 +162,7 @@ object ReadableTime { fun getFilenamableTime(time: Long): String { synchronized(sDateFormatLock) { return FILENAMABLE_DATE_FORMAT.format( - Instant.ofEpochMilli(time).atZone( - ZoneId.systemDefault(), - ), + time.toLocalDateTime(TimeZone.currentSystemDefault()), ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef0391f35..4650c8ad4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ compose-destinations = "1.10.1" google-android-material = "1.12.0-alpha03" kotlin = "2.0.0-Beta4" kotlinx-coroutines = "1.8.0" -kotlinx-datetime = "0.5.0" +kotlinx-datetime = "0.6.0-RC" kotlinx-serialization = "1.6.3" ksp = "2.0.0-Beta4-1.0.18" ktor = "3.0.0-beta-1"