Files
Dantotsu/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt
2024-07-07 01:59:24 +01:00

473 lines
20 KiB
Kotlin

package ani.dantotsu.profile
import android.animation.ObjectAnimator
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Bundle
import android.util.Base64
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupMenu
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.R
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
import ani.dantotsu.navBarHeight
import ani.dantotsu.openImage
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.profile.activity.ActivityFragment
import ani.dantotsu.profile.activity.ActivityFragment.Companion.ActivityType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import nl.joery.animatedbottombar.AnimatedBottomBar
import java.io.ByteArrayOutputStream
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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
val context = this
screenWidth = resources.displayMetrics.widthPixels.toFloat()
navBar = binding.profileNavBar
val navBarRightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) navBarHeight else 0
val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = navBarRightMargin
bottomMargin = navBarBottomMargin
}
val feedTab = navBar.createTab(R.drawable.ic_round_filter_24, "Feed")
val profileTab = navBar.createTab(R.drawable.ic_round_person_24, "Profile")
val statsTab = navBar.createTab(R.drawable.ic_stats_24, "Stats")
navBar.addTab(profileTab)
navBar.addTab(feedTab)
navBar.addTab(statsTab)
navBar.visibility = View.GONE
binding.profileViewPager.isUserInputEnabled = false
bindingProfileAppBar = ItemProfileAppBarBinding.bind(binding.root)
lifecycleScope.launch(Dispatchers.IO) {
val userid = intent.getIntExtra("userId", -1)
val username = intent.getStringExtra("username") ?: ""
val respond =
if (userid != -1) Anilist.query.getUserProfile(userid) else
Anilist.query.getUserProfile(username)
val user = respond?.data?.user
if (user == null) {
toast("User not found")
finish()
return@launch
}
withContext(Dispatchers.Main) {
binding.profileViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.profileViewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle, user)
binding.profileViewPager.setOffscreenPageLimit(3)
binding.profileViewPager.setCurrentItem(selected, false)
navBar.visibility = View.VISIBLE
navBar.selectTabAt(selected)
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = newIndex
binding.profileViewPager.setCurrentItem(selected, true)
}
})
bindingProfileAppBar = ItemProfileAppBarBinding.bind(binding.root).apply {
val intent = Intent(context, CookieCatcher::class.java)
.putExtra("url", "https://anilist.co/user/${Anilist.userid}")
.putExtra("headers", "{\"Authorization: ${Anilist.token}\"")
ContextCompat.startActivity(context, intent, null)
binding.profileProgressBar.visibility = View.GONE
editProfileAvatar.visibility = View.GONE
editProfileBanner.visibility = View.GONE
bindingProfileAppBar.editProfileAvatar.setOnClickListener {
openMediaPickerForAvatar()
}
bindingProfileAppBar.editProfileBanner.setOnClickListener {
openMediaPickerForBanner()
}
binding.apply {
editProfileSave?.setOnClickListener { saveProfileImages() }
}
followButton.isGone =
user.id == Anilist.userid || Anilist.userid == null
fun followText(): String {
return getString(
when {
user.isFollowing && user.isFollower -> R.string.mutual
user.isFollowing -> R.string.unfollow
user.isFollower -> R.string.follows_you
else -> R.string.follow
}
)
}
followButton.text = followText()
followButton.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val res = Anilist.mutation.toggleFollow(user.id)
if (res?.data?.toggleFollow != null) {
withContext(Dispatchers.Main) {
snackString(R.string.success)
user.isFollowing = res.data.toggleFollow.isFollowing
followButton.text = followText()
}
}
}
}
profileAppBar.visibility = View.VISIBLE
profileMenuButton.setOnClickListener {
val popup = PopupMenu(context, profileMenuButton)
popup.menuInflater.inflate(R.menu.menu_profile, popup.menu)
if (user.id != Anilist.userid) {
popup.menu.findItem(R.id.action_edit_profile)?.isVisible = false
}
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_view_on_anilist -> {
openLinkInBrowser("https://anilist.co/user/${user.name}")
true
}
R.id.action_edit_profile -> {
toggleEditProfile()
true
}
else -> false
}
}
popup.show()
}
profileUserAvatar.loadImage(user.avatar?.medium)
profileUserAvatar.openImage(
context.getString(R.string.avatar, user.name),
user.avatar?.medium ?: ""
)
profileUserName.text = user.name
val bannerAnimations: ImageView =
if (PrefManager.getVal(PrefName.BannerAnimations)) profileBannerImage else profileBannerImageNoKen
blurImage(
bannerAnimations,
user.bannerImage ?: user.avatar?.medium
)
profileBannerImage.updateLayoutParams { height += statusBarHeight }
profileBannerImageNoKen.updateLayoutParams { height += statusBarHeight }
profileBannerGradient.updateLayoutParams { height += statusBarHeight }
profileCloseButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
profileMenuButton.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
profileButtonContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
profileBannerImage.openImage(
context.getString(R.string.banner, user.name),
user.bannerImage ?: user.avatar?.medium ?: ""
)
mMaxScrollSize = profileAppBar.totalScrollRange
profileAppBar.addOnOffsetChangedListener(context)
profileFollowerCount.text =
(respond.data.followerPage?.pageInfo?.total ?: 0).toString()
profileFollowerCountContainer.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, FollowActivity::class.java)
.putExtra("title", getString(R.string.followers))
.putExtra("userId", user.id),
null
)
}
profileFollowingCount.text =
(respond.data.followingPage?.pageInfo?.total ?: 0).toString()
profileFollowingCountContainer.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, FollowActivity::class.java)
.putExtra("title", "Following")
.putExtra("userId", user.id),
null
)
}
profileAnimeCount.text = user.statistics.anime.count.toString()
profileAnimeCountContainer.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ListActivity::class.java)
.putExtra("anime", true)
.putExtra("userId", user.id)
.putExtra("username", user.name),
null
)
}
profileMangaCount.text = user.statistics.manga.count.toString()
profileMangaCountContainer.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ListActivity::class.java)
.putExtra("anime", false)
.putExtra("userId", user.id)
.putExtra("username", user.name),
null
)
}
profileCloseButton.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
}
}
}
private fun toggleEditProfile() {
val viewIds = arrayOf(
R.id.profileNavBar,
R.id.profileButtonContainer,
R.id.userStatsContainer,
R.id.profileFavAnimeContainer,
R.id.profileFavMangaContainer,
R.id.profileFavCharactersContainer,
//R.id.profileFavStaffContainer
R.id.imageStatsContainer,
R.id.editProfileAvatar,
R.id.editProfileBanner,
R.id.editProfileSave,
)
viewIds.forEach { viewId ->
findViewById<View>(viewId)?.apply {
visibility = if (visibility == View.VISIBLE) View.GONE else View.VISIBLE
}
}
}
private val avatarPicker =
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? ->
if (uri != null) {
val bitmap = getBitmapFromUri(uri)
bindingProfileAppBar.profileUserAvatar.setImageBitmap(bitmap)
}
}
private val bannerPicker =
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? ->
if (uri != null) {
val bitmap = getBitmapFromUri(uri)
bindingProfileAppBar.profileBannerImage.setImageBitmap(bitmap)
}
}
private fun openMediaPickerForAvatar() {
avatarPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
private fun openMediaPickerForBanner() {
bannerPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
private fun getBitmapFromUri(uri: Uri): Bitmap {
val parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")
val fileDescriptor = parcelFileDescriptor?.fileDescriptor
val image = BitmapFactory.decodeFileDescriptor(fileDescriptor)
parcelFileDescriptor?.close()
return image
}
private fun saveProfileImages() {
val bannerBitmap = (bindingProfileAppBar.profileBannerImage.drawable as? BitmapDrawable)?.bitmap
if (avatarBitmap != null && bannerBitmap != null) {
uploadAvatar(avatarBitmap)
uploadBanner(bannerBitmap)
} else {
toast("Please select both avatar and banner images")
}
}
private fun uploadAvatar(bitmap: Bitmap) {
val base64Avatar = bitmapToBase64(bitmap)
lifecycleScope.launch(Dispatchers.IO) {
val response = Anilist.mutation.saveUserAvatar(base64Avatar)
withContext(Dispatchers.Main) {
handleApiResponse(response, "Avatar")
}
}
}
private fun uploadBanner(bitmap: Bitmap) {
val base64Banner = bitmapToBase64(bitmap)
lifecycleScope.launch(Dispatchers.IO) {
val response = Anilist.mutation.saveUserBanner(base64Banner)
withContext(Dispatchers.Main) {
handleApiResponse(response, "Banner")
}
}
}
private fun bitmapToBase64(bitmap: Bitmap): String {
val outputStream = ByteArrayOutputStream()
val imageFormat = when (bitmap.config) {
Bitmap.Config.ARGB_8888 -> "png"
Bitmap.Config.RGB_565 -> "png"
Bitmap.Config.ALPHA_8 -> "png"
else -> "jpeg"
}
bitmap.compress(
if (imageFormat == "png") Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG,
100,
outputStream
)
val base64 = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
return "data:image/$imageFormat;base64,$base64"
}
private fun handleApiResponse(response: kotlinx.serialization.json.JsonObject?, type: String) {
val errors = response?.get("errors")?.jsonArray
if (!errors.isNullOrEmpty()) {
val errorMessage = errors.joinToString(separator = "\n") { it.jsonObject["message"]?.jsonPrimitive?.content ?: "Unknown error" }
toast("Error uploading $type: $errorMessage")
} else {
toast("$type uploaded successfully")
}
}
private var isCollapsed = false
private val percent = 65
private var mMaxScrollSize = 0
private var screenWidth: Float = 0f
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize
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)) profileBannerImage.resume()
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val rightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) navBarHeight else 0
val bottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
val params: ViewGroup.MarginLayoutParams =
navBar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(right = rightMargin, bottom = bottomMargin)
}
override fun onResume() {
if (this::navBar.isInitialized) {
navBar.selectTabAt(selected)
}
super.onResume()
}
private class ViewPagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
private val user: Query.UserProfile
) :
FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment = when (position) {
0 -> ProfileFragment.newInstance(user)
1 -> ActivityFragment.newInstance(ActivityType.OTHER_USER, user.id)
2 -> StatsFragment.newInstance(user)
else -> ProfileFragment.newInstance(user)
}
}
}