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/profile/ProfileActivity.kt b/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt index 90fd0278..571bc2a5 100644 --- a/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt @@ -6,7 +6,6 @@ import android.content.res.Configuration import android.os.Bundle import android.view.View import android.view.ViewGroup -import android.widget.ImageView import android.widget.PopupMenu import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -23,6 +22,7 @@ import ani.dantotsu.blurImage import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.databinding.ActivityProfileBinding +import ani.dantotsu.databinding.ItemProfileAppBarBinding import ani.dantotsu.initActivity import ani.dantotsu.loadImage import ani.dantotsu.media.user.ListActivity @@ -46,6 +46,7 @@ import kotlin.math.abs class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener { lateinit var binding: ActivityProfileBinding + private lateinit var bindingProfileAppBar: ItemProfileAppBarBinding private var selected: Int = 0 lateinit var navBar: AnimatedBottomBar @@ -109,147 +110,165 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene binding.profileViewPager.setCurrentItem(selected, true) } }) - val userLevel = intent.getStringExtra("userLVL") ?: "" - binding.followButton.isGone = user.id == Anilist.userid || Anilist.userid == null - binding.followButton.text = getString( - when { - user.isFollowing -> R.string.unfollow - user.isFollower -> R.string.follows_you - else -> R.string.follow - } - ) - if (user.isFollowing && user.isFollower) binding.followButton.text = getString(R.string.mutual) - binding.followButton.setOnClickListener { - lifecycleScope.launch(Dispatchers.IO) { - val res = Anilist.query.toggleFollow(user.id) - if (res?.data?.toggleFollow != null) { - withContext(Dispatchers.Main) { - snackString(R.string.success) - user.isFollowing = res.data.toggleFollow.isFollowing - binding.followButton.text = getString( - when { - user.isFollowing -> R.string.unfollow - user.isFollower -> R.string.follows_you - else -> R.string.follow - } - ) - if (user.isFollowing && user.isFollower) - binding.followButton.text = getString(R.string.mutual) + + bindingProfileAppBar = ItemProfileAppBarBinding.bind(binding.root).apply { + + val userLevel = intent.getStringExtra("userLVL") ?: "" + followButton.isGone = + user.id == Anilist.userid || Anilist.userid == null + followButton.text = getString( + when { + user.isFollowing -> R.string.unfollow + user.isFollower -> R.string.follows_you + else -> R.string.follow + } + ) + if (user.isFollowing && user.isFollower) followButton.text = + getString(R.string.mutual) + followButton.setOnClickListener { + lifecycleScope.launch(Dispatchers.IO) { + val res = Anilist.query.toggleFollow(user.id) + if (res?.data?.toggleFollow != null) { + withContext(Dispatchers.Main) { + snackString(R.string.success) + user.isFollowing = res.data.toggleFollow.isFollowing + followButton.text = getString( + when { + user.isFollowing -> R.string.unfollow + user.isFollower -> R.string.follows_you + else -> R.string.follow + } + ) + if (user.isFollowing && user.isFollower) + followButton.text = getString(R.string.mutual) + } } } } - } - binding.profileProgressBar.visibility = View.GONE - binding.profileAppBar.visibility = View.VISIBLE - binding.profileMenuButton.setOnClickListener { - val popup = PopupMenu(this@ProfileActivity, binding.profileMenuButton) - popup.menuInflater.inflate(R.menu.menu_profile, popup.menu) - popup.setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.action_view_following -> { - ContextCompat.startActivity( - this@ProfileActivity, - Intent(this@ProfileActivity, FollowActivity::class.java) - .putExtra("title", "Following") - .putExtra("userId", user.id), - null - ) - true - } + binding.profileProgressBar.visibility = View.GONE + profileAppBar.visibility = View.VISIBLE + profileMenuButton.setOnClickListener { + val popup = PopupMenu(this@ProfileActivity, profileMenuButton) + popup.menuInflater.inflate(R.menu.menu_profile, popup.menu) + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.action_view_following -> { + ContextCompat.startActivity( + this@ProfileActivity, + Intent(this@ProfileActivity, FollowActivity::class.java) + .putExtra("title", "Following") + .putExtra("userId", user.id), + null + ) + true + } - R.id.action_view_followers -> { - ContextCompat.startActivity( - this@ProfileActivity, - Intent(this@ProfileActivity, FollowActivity::class.java) - .putExtra("title", "Followers") - .putExtra("userId", user.id), - null - ) - true - } + R.id.action_view_followers -> { + ContextCompat.startActivity( + this@ProfileActivity, + Intent(this@ProfileActivity, FollowActivity::class.java) + .putExtra("title", "Followers") + .putExtra("userId", user.id), + null + ) + true + } - R.id.action_view_on_anilist -> { - openLinkInBrowser("https://anilist.co/user/${user.name}") - true - } + R.id.action_view_on_anilist -> { + openLinkInBrowser("https://anilist.co/user/${user.name}") + true + } - else -> false + else -> false + } } + popup.show() } - popup.show() - } - binding.profileUserAvatar.loadImage(user.avatar?.medium) - binding.profileUserAvatar.setOnLongClickListener { - ImageViewDialog.newInstance( - this@ProfileActivity, - "${user.name}'s [Avatar]", - user.avatar?.medium + profileUserAvatar.loadImage(user.avatar?.medium) + profileUserAvatar.setOnLongClickListener { + ImageViewDialog.newInstance( + this@ProfileActivity, + "${user.name}'s [Avatar]", + user.avatar?.medium + ) + } + + val userLevelText = "${user.name} $userLevel" + profileUserName.text = userLevelText + val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations) + + blurImage( + if (bannerAnimations) profileBannerImage else profileBannerImageNoKen, + user.bannerImage ?: user.avatar?.medium ) - } + profileBannerImage.updateLayoutParams { height += statusBarHeight } + profileBannerImageNoKen.updateLayoutParams { height += statusBarHeight } + profileBannerGradient.updateLayoutParams { height += statusBarHeight } + profileCloseButton.updateLayoutParams { topMargin += statusBarHeight } + profileMenuButton.updateLayoutParams { topMargin += statusBarHeight } + profileButtonContainer.updateLayoutParams { topMargin += statusBarHeight } + profileBannerImage.setOnLongClickListener { + ImageViewDialog.newInstance( + this@ProfileActivity, + user.name + " [Banner]", + user.bannerImage + ) + } - val userLevelText = "${user.name} $userLevel" - binding.profileUserName.text = userLevelText - val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations) - - blurImage(if (bannerAnimations) binding.profileBannerImage else binding.profileBannerImageNoKen as ImageView, user.bannerImage ?: user.avatar?.medium) - binding.profileBannerImage.updateLayoutParams { height += statusBarHeight } - binding.profileBannerImageNoKen?.updateLayoutParams { height += statusBarHeight } - binding.profileBannerGradient.updateLayoutParams { height += statusBarHeight } - binding.profileMenuButton.updateLayoutParams { topMargin += statusBarHeight } - binding.profileButtonContainer.updateLayoutParams { topMargin += statusBarHeight } - binding.profileBannerImage.setOnLongClickListener { - ImageViewDialog.newInstance( - this@ProfileActivity, - user.name + " [Banner]", - user.bannerImage - ) - } - - mMaxScrollSize = binding.profileAppBar.totalScrollRange - binding.profileAppBar.addOnOffsetChangedListener(this@ProfileActivity) + mMaxScrollSize = profileAppBar.totalScrollRange + profileAppBar.addOnOffsetChangedListener(this@ProfileActivity) - binding.profileFollowerCount.text = followers.toString() - binding.profileFollowerCountContainer.setOnClickListener { - ContextCompat.startActivity( - this@ProfileActivity, - Intent(this@ProfileActivity, FollowActivity::class.java) - .putExtra("title", getString(R.string.followers)) - .putExtra("userId", user.id), - null - ) - } + profileFollowerCount.text = followers.toString() + profileFollowerCountContainer.setOnClickListener { + ContextCompat.startActivity( + this@ProfileActivity, + Intent(this@ProfileActivity, FollowActivity::class.java) + .putExtra("title", getString(R.string.followers)) + .putExtra("userId", user.id), + null + ) + } - binding.profileFollowingCount.text = following.toString() - binding.profileFollowingCountContainer.setOnClickListener { - ContextCompat.startActivity( - this@ProfileActivity, - Intent(this@ProfileActivity, FollowActivity::class.java) - .putExtra("title", "Following") - .putExtra("userId", user.id), - null - ) - } + profileFollowingCount.text = following.toString() + profileFollowingCountContainer.setOnClickListener { + ContextCompat.startActivity( + this@ProfileActivity, + Intent(this@ProfileActivity, FollowActivity::class.java) + .putExtra("title", "Following") + .putExtra("userId", user.id), + null + ) + } - binding.profileAnimeCount.text = user.statistics.anime.count.toString() - binding.profileAnimeCountContainer.setOnClickListener { - ContextCompat.startActivity( - this@ProfileActivity, Intent(this@ProfileActivity, ListActivity::class.java) - .putExtra("anime", true) - .putExtra("userId", user.id) - .putExtra("username", user.name), null - ) - } + profileAnimeCount.text = user.statistics.anime.count.toString() + profileAnimeCountContainer.setOnClickListener { + ContextCompat.startActivity( + this@ProfileActivity, + Intent(this@ProfileActivity, ListActivity::class.java) + .putExtra("anime", true) + .putExtra("userId", user.id) + .putExtra("username", user.name), + null + ) + } - binding.profileMangaCount.text = user.statistics.manga.count.toString() - binding.profileMangaCountContainer.setOnClickListener { - ContextCompat.startActivity( - this@ProfileActivity, Intent(this@ProfileActivity, ListActivity::class.java) - .putExtra("anime", false) - .putExtra("userId", user.id) - .putExtra("username", user.name), null - ) + profileMangaCount.text = user.statistics.manga.count.toString() + profileMangaCountContainer.setOnClickListener { + ContextCompat.startActivity( + this@ProfileActivity, + Intent(this@ProfileActivity, ListActivity::class.java) + .putExtra("anime", false) + .putExtra("userId", user.id) + .putExtra("username", user.name), + null + ) + } + + profileCloseButton.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } } } } @@ -265,29 +284,31 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange val percentage = abs(i) * 100 / mMaxScrollSize - binding.profileUserAvatarContainer.visibility = - if (binding.profileUserAvatarContainer.scaleX == 0f) View.GONE else View.VISIBLE - val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong() - if (percentage >= percent && !isCollapsed) { - isCollapsed = true - ObjectAnimator.ofFloat(binding.profileUserDataContainer, "translationX", screenWidth) - .setDuration(duration).start() - ObjectAnimator.ofFloat(binding.profileUserAvatarContainer, "translationX", screenWidth) - .setDuration(duration).start() - ObjectAnimator.ofFloat(binding.profileButtonContainer, "translationX", screenWidth) - .setDuration(duration).start() - binding.profileBannerImage.pause() - } - if (percentage <= percent && isCollapsed) { - isCollapsed = false - ObjectAnimator.ofFloat(binding.profileUserDataContainer, "translationX", 0f) - .setDuration(duration).start() - ObjectAnimator.ofFloat(binding.profileUserAvatarContainer, "translationX", 0f) - .setDuration(duration).start() - ObjectAnimator.ofFloat(binding.profileButtonContainer, "translationX", 0f) - .setDuration(duration).start() + with (bindingProfileAppBar) { + profileUserAvatarContainer.visibility = + if (profileUserAvatarContainer.scaleX == 0f) View.GONE else View.VISIBLE + val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong() + if (percentage >= percent && !isCollapsed) { + isCollapsed = true + ObjectAnimator.ofFloat(profileUserDataContainer, "translationX", screenWidth) + .setDuration(duration).start() + ObjectAnimator.ofFloat(profileUserAvatarContainer, "translationX", screenWidth) + .setDuration(duration).start() + ObjectAnimator.ofFloat(profileButtonContainer, "translationX", screenWidth) + .setDuration(duration).start() + profileBannerImage.pause() + } + if (percentage <= percent && isCollapsed) { + isCollapsed = false + ObjectAnimator.ofFloat(profileUserDataContainer, "translationX", 0f) + .setDuration(duration).start() + ObjectAnimator.ofFloat(profileUserAvatarContainer, "translationX", 0f) + .setDuration(duration).start() + ObjectAnimator.ofFloat(profileButtonContainer, "translationX", 0f) + .setDuration(duration).start() - if (PrefManager.getVal(PrefName.BannerAnimations)) binding.profileBannerImage.resume() + if (PrefManager.getVal(PrefName.BannerAnimations)) profileBannerImage.resume() + } } } 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/drawable/ic_circle_arrow_left_24.xml b/app/src/main/res/drawable/ic_circle_arrow_left_24.xml new file mode 100644 index 00000000..687a99d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_arrow_left_24.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/layout-land/activity_profile.xml b/app/src/main/res/layout-land/activity_profile.xml index 7e01d082..b4844201 100644 --- a/app/src/main/res/layout-land/activity_profile.xml +++ b/app/src/main/res/layout-land/activity_profile.xml @@ -24,253 +24,7 @@ android:layout_height="wrap_content" /> - - - - - - - - - - - - - - - - - - - - -