feat: add a time since chapter item (#316)

* feat: add a time since chapter item

* fix: this is the song that never ends
This commit is contained in:
TwistedUmbrellaX 2024-04-04 05:45:03 -04:00 committed by GitHub
parent 6bfadfa962
commit e1b968bfe0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 178 additions and 6 deletions

View file

@ -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<String, Genre>.checkId(id: Int): Boolean {
this.forEach {
if (it.value.id == id) {

View file

@ -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<MangaUpdatesResponse>()
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<Results>? = 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
)
}
}
}
}
}

View file

@ -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 }

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -502,6 +502,7 @@
<string name="refresh_token_load_failed">Refresh Token : Failed to load Saved Token</string>
<string name="refreshing_token_failed">Refreshing Token Failed</string>
<string name="episode_release_countdown">Episode %1$d will be released in</string>
<string name="chapter_release_timeout">Chapter %1$d has been available for</string>
<string name="time_format">%1$d days %2$d hrs %3$d mins %4$d secs</string>
<string-array name="sort_by">
<item>Score</item>