From e1b968bfe0951e769318a28761a1ff952f3bf8e5 Mon Sep 17 00:00:00 2001 From: TwistedUmbrellaX <1173913+AbandonedCart@users.noreply.github.com> Date: Thu, 4 Apr 2024 05:45:03 -0400 Subject: [PATCH] feat: add a time since chapter item (#316) * feat: add a time since chapter item * fix: this is the song that never ends --- app/src/main/java/ani/dantotsu/Functions.kt | 53 ++++++++++ .../connections/bakaupdates/MangaUpdates.kt | 98 +++++++++++++++++++ .../ani/dantotsu/media/MediaInfoFragment.kt | 5 +- .../dantotsu/media/anime/AnimeWatchAdapter.kt | 5 +- .../java/ani/dantotsu/util/CountUpTimer.kt | 22 +++++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt create mode 100644 app/src/main/java/ani/dantotsu/util/CountUpTimer.kt diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt index 9acb51f7..b5640fb5 100644 --- a/app/src/main/java/ani/dantotsu/Functions.kt +++ b/app/src/main/java/ani/dantotsu/Functions.kt @@ -92,6 +92,7 @@ import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.BuildConfig.APPLICATION_ID import ani.dantotsu.connections.anilist.Genre import ani.dantotsu.connections.anilist.api.FuzzyDate +import ani.dantotsu.connections.bakaupdates.MangaUpdates import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.databinding.ItemCountDownBinding import ani.dantotsu.media.Media @@ -102,6 +103,7 @@ import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt +import ani.dantotsu.util.CountUpTimer import ani.dantotsu.util.Logger import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder @@ -134,9 +136,12 @@ import io.noties.markwon.html.TagHandlerNoOp import io.noties.markwon.image.AsyncDrawable import io.noties.markwon.image.glide.GlideImagesPlugin import jp.wasabeef.glide.transformations.BlurTransformation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import nl.joery.animatedbottombar.AnimatedBottomBar import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -985,6 +990,54 @@ fun countDown(media: Media, view: ViewGroup) { } } +fun sinceWhen(media: Media, view: ViewGroup) { + CoroutineScope(Dispatchers.IO).launch { + MangaUpdates().search(media.name ?: media.nameRomaji, media.startDate)?.let { + val latestChapter = it.metadata.series.latestChapter ?: it.record.chapter?.let { chapter -> + if (chapter.contains("-")) + chapter.split("-")[1].trim() + else + chapter + }?.toInt() + val timeSince = (System.currentTimeMillis() - + (it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000 + + withContext(Dispatchers.Main) { + val v = + ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false) + view.addView(v.root, 0) + v.mediaCountdownText.text = + currActivity()?.getString(R.string.chapter_release_timeout, latestChapter) + + object : CountUpTimer(86400000) { + override fun onTick(second: Int) { + val a = second + timeSince + v.mediaCountdown.text = currActivity()?.getString( + R.string.time_format, + a / 86400, + a % 86400 / 3600, + a % 86400 % 3600 / 60, + a % 86400 % 3600 % 60 + ) + } + + override fun onFinish() { + // The legend will never die. + } + }.start() + } + } + } +} + +fun displayTimer(media: Media, view: ViewGroup) { + when { + media.anime != null -> countDown(media, view) + media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view) + else -> { } // No timer yet + } +} + fun MutableMap.checkId(id: Int): Boolean { this.forEach { if (it.value.id == id) { diff --git a/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt b/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt new file mode 100644 index 00000000..7d811ba3 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt @@ -0,0 +1,98 @@ +package ani.dantotsu.connections.bakaupdates + +import ani.dantotsu.client +import ani.dantotsu.connections.anilist.api.FuzzyDate +import ani.dantotsu.tryWithSuspend +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import okio.ByteString.Companion.encode +import org.json.JSONException +import org.json.JSONObject +import java.nio.charset.Charset + + +class MangaUpdates { + + private val Int?.dateFormat get() = String.format("%02d", this) + + private val apiUrl = "https://api.mangaupdates.com/v1/releases/search" + + suspend fun search(title: String, startDate: FuzzyDate?) : MangaUpdatesResponse.Results? { + return tryWithSuspend { + val query = JSONObject().apply { + try { + put("search", title.encode(Charset.forName("UTF-8"))) + startDate?.let { + put( + "start_date", + "${it.year}-${it.month.dateFormat}-${it.day.dateFormat}" + ) + } + put("include_metadata", true) + } catch (e: JSONException) { + e.printStackTrace() + } + } + val res = client.post(apiUrl, json = query).parsed() + res.results?.forEach{ println("MangaUpdates: $it") } + res.results?.first { it.metadata.series.lastUpdated?.timestamp != null } + } + } + + @Serializable + data class MangaUpdatesResponse( + @SerialName("total_hits") + val totalHits: Int?, + @SerialName("page") + val page: Int?, + @SerialName("per_page") + val perPage: Int?, + val results: List? = null + ) { + @Serializable + data class Results( + val record: Record, + val metadata: MetaData + ) { + @Serializable + data class Record( + @SerialName("id") + val id: Int, + @SerialName("title") + val title: String, + @SerialName("volume") + val volume: String?, + @SerialName("chapter") + val chapter: String?, + @SerialName("release_date") + val releaseDate: String + ) + @Serializable + data class MetaData( + val series: Series + ) { + @Serializable + data class Series( + @SerialName("series_id") + val seriesId: Long?, + @SerialName("title") + val title: String?, + @SerialName("latest_chapter") + val latestChapter: Int?, + @SerialName("last_updated") + val lastUpdated: LastUpdated? + ) { + @Serializable + data class LastUpdated( + @SerialName("timestamp") + val timestamp: Long, + @SerialName("as_rfc3339") + val asRfc3339: String, + @SerialName("as_string") + val asString: String + ) + } + } + } + } +} diff --git a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt index a7d07b2e..609de856 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt @@ -27,7 +27,6 @@ import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.GenresViewModel import ani.dantotsu.copyToClipboard -import ani.dantotsu.countDown import ani.dantotsu.currActivity import ani.dantotsu.databinding.ActivityGenreBinding import ani.dantotsu.databinding.FragmentMediaInfoBinding @@ -38,6 +37,7 @@ import ani.dantotsu.databinding.ItemTitleRecyclerBinding import ani.dantotsu.databinding.ItemTitleSearchBinding import ani.dantotsu.databinding.ItemTitleTextBinding import ani.dantotsu.databinding.ItemTitleTrailerBinding +import ani.dantotsu.displayTimer import ani.dantotsu.loadImage import ani.dantotsu.navBarHeight import ani.dantotsu.px @@ -225,8 +225,7 @@ class MediaInfoFragment : Fragment() { .setDuration(400).start() } } - - countDown(media, binding.mediaInfoContainer) + displayTimer(media, binding.mediaInfoContainer) val parent = _binding?.mediaInfoContainer!! val screenWidth = resources.displayMetrics.run { widthPixels / density } diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt index facf7cfa..d5d11b9e 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -17,11 +17,11 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.FileUrl import ani.dantotsu.R -import ani.dantotsu.countDown import ani.dantotsu.currActivity import ani.dantotsu.databinding.DialogLayoutBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemChipBinding +import ani.dantotsu.displayTimer import ani.dantotsu.isOnline import ani.dantotsu.loadImage import ani.dantotsu.media.Media @@ -500,8 +500,7 @@ class AnimeWatchAdapter( inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) { init { - //Timer - countDown(media, binding.animeSourceContainer) + displayTimer(media, binding.animeSourceContainer) } } } diff --git a/app/src/main/java/ani/dantotsu/util/CountUpTimer.kt b/app/src/main/java/ani/dantotsu/util/CountUpTimer.kt new file mode 100644 index 00000000..725781ff --- /dev/null +++ b/app/src/main/java/ani/dantotsu/util/CountUpTimer.kt @@ -0,0 +1,22 @@ +package ani.dantotsu.util + +import android.os.CountDownTimer + +// https://stackoverflow.com/a/40422151/461982 +abstract class CountUpTimer protected constructor( + private val duration: Long +) : CountDownTimer(duration, INTERVAL_MS) { + abstract fun onTick(second: Int) + override fun onTick(msUntilFinished: Long) { + val second = ((duration - msUntilFinished) / 1000).toInt() + onTick(second) + } + + override fun onFinish() { + onTick(duration / 1000) + } + + companion object { + private const val INTERVAL_MS: Long = 1000 + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d92b3512..6345675d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -502,6 +502,7 @@ Refresh Token : Failed to load Saved Token Refreshing Token Failed Episode %1$d will be released in + Chapter %1$d has been available for %1$d days %2$d hrs %3$d mins %4$d secs Score