Thorium-android-app/app/src/main/java/net/schueller/peertube/adapter/MultiViewRecyclerViewHolder.kt

563 lines
24 KiB
Kotlin
Raw Normal View History

2021-12-26 14:59:46 +01:00
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.adapter
2022-01-01 19:43:48 +01:00
import android.app.AlertDialog
2022-01-01 01:53:43 +01:00
import android.content.Context
2022-01-01 19:43:48 +01:00
import android.content.DialogInterface
2021-12-26 14:59:46 +01:00
import android.content.Intent
2022-01-01 01:53:43 +01:00
import android.util.Log
2021-12-26 14:59:46 +01:00
import android.view.MenuItem
import android.view.View
2022-01-01 01:53:43 +01:00
import android.view.View.GONE
import android.widget.Toast
2021-12-26 14:59:46 +01:00
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
2022-01-01 01:53:43 +01:00
import com.google.gson.JsonObject
2022-01-08 08:48:41 +01:00
import com.mikepenz.iconics.Iconics.Builder
2021-12-26 14:59:46 +01:00
import com.squareup.picasso.Picasso
2022-01-08 08:48:41 +01:00
import net.schueller.peertube.R
import net.schueller.peertube.R.*
2021-12-26 14:59:46 +01:00
import net.schueller.peertube.activity.AccountActivity
import net.schueller.peertube.activity.VideoListActivity
import net.schueller.peertube.activity.VideoPlayActivity
2022-01-01 01:53:43 +01:00
import net.schueller.peertube.databinding.*
import net.schueller.peertube.fragment.VideoMetaDataFragment
2022-01-08 08:48:41 +01:00
import net.schueller.peertube.helper.APIUrlHelper
2022-01-01 19:43:48 +01:00
import net.schueller.peertube.helper.MetaDataHelper.getCreatorAvatar
import net.schueller.peertube.helper.MetaDataHelper.getCreatorString
2022-01-08 08:48:41 +01:00
import net.schueller.peertube.helper.MetaDataHelper.getDuration
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
import net.schueller.peertube.helper.MetaDataHelper.isChannel
2021-12-26 14:59:46 +01:00
import net.schueller.peertube.intents.Intents
2022-01-01 01:53:43 +01:00
import net.schueller.peertube.model.*
import net.schueller.peertube.model.ui.VideoMetaViewItem
2022-01-08 08:48:41 +01:00
import net.schueller.peertube.network.GetUserService
2022-01-01 01:53:43 +01:00
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.network.Session
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
2021-12-26 14:59:46 +01:00
sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
2022-01-01 01:53:43 +01:00
var videoRating: Rating? = null
var isLeaveAppExpected = false
2022-01-08 08:48:41 +01:00
class CategoryViewHolder(private val binding: ItemCategoryTitleBinding) : MultiViewRecyclerViewHolder(binding) {
2021-12-26 14:59:46 +01:00
fun bind(category: Category) {
binding.textViewTitle.text = category.label
}
}
2022-01-08 08:48:41 +01:00
class VideoCommentsViewHolder(private val binding: ItemVideoCommentsOverviewBinding) : MultiViewRecyclerViewHolder(binding) {
2022-01-01 01:53:43 +01:00
fun bind(commentThread: CommentThread) {
binding.videoCommentsTotalCount.text = commentThread.total.toString()
if (commentThread.comments.isNotEmpty()) {
val highlightedComment: Comment = commentThread.comments[0]
// owner / creator Avatar
val avatar = highlightedComment.account.avatar
if (avatar != null) {
val baseUrl = APIUrlHelper.getUrl(binding.videoHighlightedAvatar.context)
val avatarPath = avatar.path
Picasso.get()
2022-01-08 08:48:41 +01:00
.load(baseUrl + avatarPath)
.into(binding.videoHighlightedAvatar)
2022-01-01 01:53:43 +01:00
}
binding.videoHighlightedComment.text = highlightedComment.text
}
}
}
2022-01-08 08:48:41 +01:00
class VideoMetaViewHolder(private val binding: ItemVideoMetaBinding, private val videoMetaDataFragment: VideoMetaDataFragment?) : MultiViewRecyclerViewHolder(binding) {
2022-01-01 01:53:43 +01:00
fun bind(videoMetaViewItem: VideoMetaViewItem) {
val video = videoMetaViewItem.video
if (video != null && videoMetaDataFragment != null) {
val context = binding.avatar.context
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
2022-01-08 08:48:41 +01:00
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
2022-01-01 01:53:43 +01:00
).create(
2022-01-08 08:48:41 +01:00
GetVideoDataService::class.java
2022-01-01 01:53:43 +01:00
)
val userService = RetrofitInstance.getRetrofitInstance(
2022-01-08 08:48:41 +01:00
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
2022-01-01 01:53:43 +01:00
).create(
2022-01-08 08:48:41 +01:00
GetUserService::class.java
2022-01-01 01:53:43 +01:00
)
// Title
binding.videoName.text = video.name
binding.videoOpenDescription.setOnClickListener {
videoMetaDataFragment.showDescriptionFragment(video)
}
// Thumbs up
binding.videoThumbsUpWrapper.setOnClickListener {
rateVideo(true, video, context, binding)
}
// Thumbs Down
binding.videoThumbsDownWrapper.setOnClickListener {
rateVideo(false, video, context, binding)
}
2022-01-08 08:48:41 +01:00
// Add to playlist
2022-01-01 01:53:43 +01:00
binding.videoAddToPlaylistWrapper.setOnClickListener {
2022-01-08 08:48:41 +01:00
videoMetaDataFragment.saveToPlaylist(video)
Toast.makeText(context, context.getString(string.saved_to_playlist), Toast.LENGTH_SHORT).show()
2022-01-01 01:53:43 +01:00
}
binding.videoBlockWrapper.setOnClickListener {
Toast.makeText(
2022-01-08 08:48:41 +01:00
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
2022-01-01 01:53:43 +01:00
).show()
}
binding.videoFlagWrapper.setOnClickListener {
Toast.makeText(
2022-01-08 08:48:41 +01:00
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
2022-01-01 01:53:43 +01:00
).show()
}
// video rating
videoRating = Rating()
videoRating!!.rating = RATING_NONE // default
updateVideoRating(video, binding)
// Retrieve which rating the user gave to this video
if (Session.getInstance().isLoggedIn) {
val call = videoDataService.getVideoRating(video.id)
call.enqueue(object : Callback<Rating?> {
override fun onResponse(call: Call<Rating?>, response: Response<Rating?>) {
videoRating = response.body()
updateVideoRating(video, binding)
}
override fun onFailure(call: Call<Rating?>, t: Throwable) {
// Do nothing.
}
})
}
// Share
binding.videoShare.setOnClickListener {
isLeaveAppExpected = true
Intents.Share(context, video)
}
// hide download if not supported by video
if (video.downloadEnabled) {
binding.videoDownloadWrapper.setOnClickListener {
Intents.Download(context, video)
}
} else {
binding.videoDownloadWrapper.visibility = GONE
}
2022-01-01 19:43:48 +01:00
// created at / views
binding.videoMeta.text = getMetaString(
2022-01-08 08:48:41 +01:00
video.createdAt,
video.views,
context,
true
2022-01-01 19:43:48 +01:00
)
// owner / creator
val displayNameAndHost = getOwnerString(video.account, context)
if (isChannel(video)) {
binding.videoBy.text = context.resources.getString(string.video_by_line, displayNameAndHost)
} else {
binding.videoBy.visibility = GONE
}
binding.videoOwner.text = getCreatorString(video, context)
2022-01-01 01:53:43 +01:00
// owner / creator Avatar
2022-01-01 19:43:48 +01:00
val avatar = getCreatorAvatar(video, context)
2022-01-01 01:53:43 +01:00
if (avatar != null) {
val baseUrl = APIUrlHelper.getUrl(context)
val avatarPath = avatar.path
Picasso.get()
2022-01-08 08:48:41 +01:00
.load(baseUrl + avatarPath)
.into(binding.avatar)
2022-01-01 01:53:43 +01:00
}
// videoOwnerSubscribers
2022-01-01 19:43:48 +01:00
binding.videoOwnerSubscribers.text = context.resources.getQuantityString(R.plurals.video_channel_subscribers, video.channel.followersCount, video.channel.followersCount)
// video owner click
binding.videoCreatorInfo.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(VideoListActivity.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// avatar click
binding.avatar.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(Companion.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
2022-01-01 01:53:43 +01:00
// get subscription status
var isSubscribed = false
if (Session.getInstance().isLoggedIn) {
val subChannel = video.channel.name + "@" + video.channel.host
val call = userService.subscriptionsExist(subChannel)
call.enqueue(object : Callback<JsonObject> {
override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
if (response.isSuccessful) {
// {"video.channel.name + "@" + video.channel.host":true}
if (response.body()?.get(video.channel.name + "@" + video.channel.host)!!.asBoolean) {
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
2022-01-01 19:43:48 +01:00
isSubscribed = true
2022-01-01 01:53:43 +01:00
} else {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
}
}
}
2022-01-08 08:48:41 +01:00
2022-01-01 01:53:43 +01:00
override fun onFailure(call: Call<JsonObject>, t: Throwable) {
// Do nothing.
}
})
}
2022-01-01 19:43:48 +01:00
// TODO: update subscriber count
2022-01-01 01:53:43 +01:00
binding.videoOwnerSubscribeButton.setOnClickListener {
if (Session.getInstance().isLoggedIn) {
if (!isSubscribed) {
val payload = video.channel.name + "@" + video.channel.host
val body = "{\"uri\":\"$payload\"}".toRequestBody("application/json".toMediaType())
val call = userService.subscribe(body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
2022-01-08 08:48:41 +01:00
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
2022-01-01 01:53:43 +01:00
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
isSubscribed = true
}
}
2022-01-08 08:48:41 +01:00
2022-01-01 01:53:43 +01:00
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
} else {
2022-01-01 19:43:48 +01:00
AlertDialog.Builder(context)
2022-01-08 08:48:41 +01:00
.setTitle(context.getString(string.video_sub_del_alert_title))
.setMessage(context.getString(string.video_sub_del_alert_msg))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
// Yes
val payload = video.channel.name + "@" + video.channel.host
val call = userService.unsubscribe(payload)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
isSubscribed = false
}
2022-01-01 19:43:48 +01:00
}
2022-01-08 08:48:41 +01:00
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
// No
}
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
2022-01-01 01:53:43 +01:00
}
} else {
Toast.makeText(
2022-01-08 08:48:41 +01:00
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
2022-01-01 01:53:43 +01:00
).show()
}
}
}
}
}
2022-01-08 08:48:41 +01:00
class ChannelViewHolder(private val binding: ItemChannelTitleBinding) : MultiViewRecyclerViewHolder(binding) {
2021-12-26 14:59:46 +01:00
fun bind(channel: Channel) {
val context = binding.avatar.context
val baseUrl = APIUrlHelper.getUrl(context)
// Avatar
val avatar: Avatar? = channel.avatar
if (avatar != null) {
val avatarPath = avatar.path
Picasso.get()
2022-01-08 08:48:41 +01:00
.load(baseUrl + avatarPath)
.placeholder(R.drawable.test_image)
.into(binding.avatar)
2021-12-26 14:59:46 +01:00
}
binding.textViewTitle.text = channel.displayName
}
}
2022-01-08 08:48:41 +01:00
class TagViewHolder(private val binding: ItemTagTitleBinding) : MultiViewRecyclerViewHolder(binding) {
2021-12-26 14:59:46 +01:00
fun bind(tag: TagVideo) {
binding.textViewTitle.text = tag.tag
}
}
2022-01-08 08:48:41 +01:00
class VideoViewHolder(private val binding: RowVideoListBinding) : MultiViewRecyclerViewHolder(binding) {
2021-12-26 14:59:46 +01:00
fun bind(video: Video) {
val context = binding.thumb.context
val baseUrl = APIUrlHelper.getUrl(context)
// Temp Loading Image
Picasso.get()
2022-01-08 08:48:41 +01:00
.load(baseUrl + video.previewPath)
.placeholder(R.drawable.test_image)
.error(R.drawable.test_image)
.into(binding.thumb)
2021-12-26 14:59:46 +01:00
// Avatar
2022-01-01 19:43:48 +01:00
val avatar = getCreatorAvatar(video, context)
2021-12-26 14:59:46 +01:00
if (avatar != null) {
val avatarPath = avatar.path
Picasso.get()
2022-01-08 08:48:41 +01:00
.load(baseUrl + avatarPath)
.into(binding.avatar)
2021-12-26 14:59:46 +01:00
}
// set Name
binding.slRowName.text = video.name
// set duration (if not live stream)
if (video.live) {
binding.videoDuration.setText(string.video_list_live_marker)
binding.videoDuration.setBackgroundColor(ContextCompat.getColor(context, color.durationLiveBackgroundColor))
} else {
binding.videoDuration.text = getDuration(video.duration.toLong())
binding.videoDuration.setBackgroundColor(ContextCompat.getColor(context, color.durationBackgroundColor))
}
// set age and view count
binding.videoMeta.text = getMetaString(
2022-01-08 08:48:41 +01:00
video.createdAt,
video.views,
context
2021-12-26 14:59:46 +01:00
)
// set owner
2022-01-01 19:43:48 +01:00
val displayNameAndHost = getOwnerString(video.account, context, true)
binding.videoOwner.text = getCreatorString(video, context, true)
2021-12-26 14:59:46 +01:00
// video owner click
binding.videoOwner.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(VideoListActivity.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// avatar click
binding.avatar.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(Companion.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// Video Click
binding.root.setOnClickListener {
val intent = Intent(context, VideoPlayActivity::class.java)
intent.putExtra(Companion.EXTRA_VIDEOID, video.uuid)
context.startActivity(intent)
}
// More Button
binding.moreButton.setText(string.video_more_icon)
Builder().on(binding.moreButton).build()
binding.moreButton.setOnClickListener { v: View? ->
val popup = PopupMenu(
2022-01-08 08:48:41 +01:00
context,
v!!
2021-12-26 14:59:46 +01:00
)
popup.setOnMenuItemClickListener { menuItem: MenuItem ->
when (menuItem.itemId) {
id.menu_share -> {
Intents.Share(context, video)
return@setOnMenuItemClickListener true
}
else -> return@setOnMenuItemClickListener false
}
}
popup.inflate(menu.menu_video_row_mode)
popup.show()
}
}
}
2022-01-01 01:53:43 +01:00
fun updateVideoRating(video: Video?, binding: ItemVideoMetaBinding) {
when (videoRating!!.rating) {
RATING_NONE -> {
Log.v("MWCVH", "RATING_NONE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
}
RATING_LIKE -> {
Log.v("MWCVH", "RATING_LIKE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up_filled)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
}
RATING_DISLIKE -> {
Log.v("MWCVH", "RATING_DISLIKE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down_filled)
}
}
// Update the texts
binding.videoThumbsUpTotal.text = video?.likes.toString()
binding.videoThumbsDownTotal.text = video?.dislikes.toString()
}
/**
* TODO: move this out and get update when rating changes
*/
fun rateVideo(like: Boolean, video: Video, context: Context, binding: ItemVideoMetaBinding) {
if (Session.getInstance().isLoggedIn) {
val ratePayload: String = when (videoRating!!.rating) {
RATING_LIKE -> if (like) RATING_NONE else RATING_DISLIKE
RATING_DISLIKE -> if (like) RATING_LIKE else RATING_NONE
RATING_NONE -> if (like) RATING_LIKE else RATING_DISLIKE
else -> if (like) RATING_LIKE else RATING_DISLIKE
}
val body = "{\"rating\":\"$ratePayload\"}".toRequestBody("application/json".toMediaType())
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
2022-01-08 08:48:41 +01:00
apiBaseURL, APIUrlHelper.useInsecureConnection(
2022-01-01 01:53:43 +01:00
context
2022-01-08 08:48:41 +01:00
)
2022-01-01 01:53:43 +01:00
).create(
2022-01-08 08:48:41 +01:00
GetVideoDataService::class.java
2022-01-01 01:53:43 +01:00
)
val call = videoDataService.rateVideo(video.id, body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
2022-01-08 08:48:41 +01:00
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
2022-01-01 01:53:43 +01:00
) {
// if 20x, update likes/dislikes
if (response.isSuccessful) {
val previousRating = videoRating!!.rating
// Update the likes/dislikes count of the video, if needed.
// This is only a visual trick, as the actual like/dislike count has
// already been modified on the PeerTube instance.
if (previousRating != ratePayload) {
when (previousRating) {
RATING_NONE -> if (ratePayload == RATING_LIKE) {
video.likes = video.likes + 1
} else {
video.dislikes = video.dislikes + 1
}
RATING_LIKE -> {
video.likes = video.likes - 1
if (ratePayload == RATING_DISLIKE) {
video.dislikes = video.dislikes + 1
}
}
RATING_DISLIKE -> {
video.dislikes = video.dislikes - 1
if (ratePayload == RATING_LIKE) {
video.likes = video.likes + 1
}
}
}
}
videoRating!!.rating = ratePayload
updateVideoRating(video, binding)
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Toast.makeText(
2022-01-08 08:48:41 +01:00
context,
context.getString(string.video_rating_failed),
Toast.LENGTH_SHORT
2022-01-01 01:53:43 +01:00
).show()
}
})
} else {
Toast.makeText(
2022-01-08 08:48:41 +01:00
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
2022-01-01 01:53:43 +01:00
).show()
}
}
companion object {
private const val RATING_NONE = "none"
private const val RATING_LIKE = "like"
private const val RATING_DISLIKE = "dislike"
const val EXTRA_VIDEOID = "VIDEOID"
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
}
2021-12-26 14:59:46 +01:00
}