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:
parent
6bfadfa962
commit
e1b968bfe0
6 changed files with 178 additions and 6 deletions
|
@ -92,6 +92,7 @@ import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||||
import ani.dantotsu.connections.anilist.Genre
|
import ani.dantotsu.connections.anilist.Genre
|
||||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||||
|
import ani.dantotsu.connections.bakaupdates.MangaUpdates
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||||
import ani.dantotsu.media.Media
|
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.PrefName
|
||||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||||
|
import ani.dantotsu.util.CountUpTimer
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestBuilder
|
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.AsyncDrawable
|
||||||
import io.noties.markwon.image.glide.GlideImagesPlugin
|
import io.noties.markwon.image.glide.GlideImagesPlugin
|
||||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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 {
|
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
|
||||||
this.forEach {
|
this.forEach {
|
||||||
if (it.value.id == id) {
|
if (it.value.id == id) {
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||||
import ani.dantotsu.copyToClipboard
|
import ani.dantotsu.copyToClipboard
|
||||||
import ani.dantotsu.countDown
|
|
||||||
import ani.dantotsu.currActivity
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||||
import ani.dantotsu.databinding.FragmentMediaInfoBinding
|
import ani.dantotsu.databinding.FragmentMediaInfoBinding
|
||||||
|
@ -38,6 +37,7 @@ import ani.dantotsu.databinding.ItemTitleRecyclerBinding
|
||||||
import ani.dantotsu.databinding.ItemTitleSearchBinding
|
import ani.dantotsu.databinding.ItemTitleSearchBinding
|
||||||
import ani.dantotsu.databinding.ItemTitleTextBinding
|
import ani.dantotsu.databinding.ItemTitleTextBinding
|
||||||
import ani.dantotsu.databinding.ItemTitleTrailerBinding
|
import ani.dantotsu.databinding.ItemTitleTrailerBinding
|
||||||
|
import ani.dantotsu.displayTimer
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.px
|
import ani.dantotsu.px
|
||||||
|
@ -225,8 +225,7 @@ class MediaInfoFragment : Fragment() {
|
||||||
.setDuration(400).start()
|
.setDuration(400).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
displayTimer(media, binding.mediaInfoContainer)
|
||||||
countDown(media, binding.mediaInfoContainer)
|
|
||||||
val parent = _binding?.mediaInfoContainer!!
|
val parent = _binding?.mediaInfoContainer!!
|
||||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,11 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.FileUrl
|
import ani.dantotsu.FileUrl
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.countDown
|
|
||||||
import ani.dantotsu.currActivity
|
import ani.dantotsu.currActivity
|
||||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
|
import ani.dantotsu.displayTimer
|
||||||
import ani.dantotsu.isOnline
|
import ani.dantotsu.isOnline
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
@ -500,8 +500,7 @@ class AnimeWatchAdapter(
|
||||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
//Timer
|
displayTimer(media, binding.animeSourceContainer)
|
||||||
countDown(media, binding.animeSourceContainer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
22
app/src/main/java/ani/dantotsu/util/CountUpTimer.kt
Normal file
22
app/src/main/java/ani/dantotsu/util/CountUpTimer.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -502,6 +502,7 @@
|
||||||
<string name="refresh_token_load_failed">Refresh Token : Failed to load Saved Token</string>
|
<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="refreshing_token_failed">Refreshing Token Failed</string>
|
||||||
<string name="episode_release_countdown">Episode %1$d will be released in</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 name="time_format">%1$d days %2$d hrs %3$d mins %4$d secs</string>
|
||||||
<string-array name="sort_by">
|
<string-array name="sort_by">
|
||||||
<item>Score</item>
|
<item>Score</item>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue