add voting system

This commit is contained in:
Cloudburst 2022-09-05 18:45:13 +02:00
parent ae137f4a34
commit a3eef399a9
11 changed files with 560 additions and 2 deletions

View file

@ -0,0 +1,86 @@
package com.lagradost.cloudstream3.plugins
import android.util.Log
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import java.security.MessageDigest
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
object VotingApi { // please do not cheat the votes lol
private const val LOGKEY = "VotingApi"
enum class VoteType(val value: Int) {
UPVOTE(1),
DOWNVOTE(-1),
NONE(0)
}
private val apiDomain = "https://api.countapi.xyz"
private fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest
.getInstance("SHA-1")
.digest("${url}#funny-salt".toByteArray())
.fold("") { str, it -> str + "%02x".format(it) }
suspend fun SitePlugin.getVotes(): Int {
if (repositoryUrl == null) return 0
return getVotes(repositoryUrl, url)
}
suspend fun SitePlugin.vote(requestType: VoteType): Int {
if (repositoryUrl == null) return 0
return vote(repositoryUrl, url, requestType)
}
fun SitePlugin.getVoteType(): VoteType {
if (repositoryUrl == null) return VoteType.NONE
return getVoteType(repositoryUrl, url)
}
suspend fun getVotes(repositoryUrl: String, pluginUrl: String): Int {
val url = "${apiDomain}/get/cs3-votes-${transformUrl(repositoryUrl)}/${transformUrl(pluginUrl)}"
Log.d(LOGKEY, "Requesting: $url")
return app.get(url).parsedSafe<Result>()?.value ?: (0.also {
ioSafe {
createBucket(repositoryUrl, pluginUrl)
}
})
}
fun getVoteType(repositoryUrl: String, pluginUrl: String): VoteType {
return getKey("cs3-votes-${transformUrl(repositoryUrl)}/${transformUrl(pluginUrl)}") ?: VoteType.NONE
}
private suspend fun createBucket(repositoryUrl: String, pluginUrl: String) {
val url = "${apiDomain}/create?namespace=cs3-votes-${transformUrl(repositoryUrl)}&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0"
Log.d(LOGKEY, "Requesting: $url")
app.get(url)
}
suspend fun vote(repositoryUrl: String, pluginUrl: String, requestType: VoteType): Int {
val savedType: VoteType = getKey("cs3-votes-${transformUrl(repositoryUrl)}/${transformUrl(pluginUrl)}") ?: VoteType.NONE
var newType: VoteType = requestType
var changeValue = 0
if (requestType == savedType) {
newType = VoteType.NONE
changeValue = -requestType.value
} else if (savedType == VoteType.NONE) {
changeValue = requestType.value
} else if (savedType != requestType) {
changeValue = -savedType.value + requestType.value
}
val url = "${apiDomain}/update/cs3-votes-${transformUrl(repositoryUrl)}/${transformUrl(pluginUrl)}?amount=${changeValue}"
Log.d(LOGKEY, "Requesting: $url")
val res = app.get(url).parsedSafe<Result>()?.value
if (res != null) {
setKey("cs3-votes-${transformUrl(repositoryUrl)}/${transformUrl(pluginUrl)}", newType)
}
return res ?: 0
}
private data class Result(
val value: Int?
)
}

View file

@ -4,10 +4,8 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.LayoutRes
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar

View file

@ -5,17 +5,22 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.GlideApp
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -23,6 +28,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.repository_item.view.*
import org.junit.Assert
import org.junit.Test
import java.text.DecimalFormat
data class PluginViewData(
@ -101,6 +107,23 @@ class PluginAdapter(
private val iconSize by lazy {
findClosestBase2(iconSizeExact, 16, 512)
}
fun prettyCount(number: Number): String? {
val suffix = charArrayOf(' ', 'k', 'M', 'B', 'T', 'P', 'E')
val numValue = number.toLong()
val value = Math.floor(Math.log10(numValue.toDouble())).toInt()
val base = value / 3
return if (value >= 3 && base < suffix.size) {
DecimalFormat("#0.00").format(
numValue / Math.pow(
10.0,
(base * 3).toDouble()
)
) + suffix[base]
} else {
DecimalFormat().format(numValue)
}
}
}
inner class PluginViewHolder(itemView: View) :
@ -112,6 +135,7 @@ class PluginAdapter(
val metadata = data.plugin.second
val disabled = metadata.status == PROVIDER_STATUS_DOWN
val alpha = if (disabled) 0.6f else 1f
val isLocal = data.plugin.second.repositoryUrl == null
itemView.main_text?.alpha = alpha
itemView.sub_text?.alpha = alpha
@ -125,6 +149,13 @@ class PluginAdapter(
itemView.action_button?.setOnClickListener {
iconClickCallback.invoke(data.plugin)
}
itemView.setOnClickListener {
if (isLocal) return@setOnClickListener
val sheet = PluginDetailsFragment(data)
val activity = itemView.context.getActivity() as AppCompatActivity
sheet.show(activity.supportFragmentManager, "PluginDetails")
}
//if (itemView.context?.isTrueTvSettings() == false) {
// val siteUrl = metadata.repositoryUrl
// if (siteUrl != null && siteUrl.isNotBlank() && siteUrl != "NONE") {
@ -185,6 +216,11 @@ class PluginAdapter(
itemView.lang_icon.text = fromTwoLettersToLanguage(metadata.language)
}
ioSafe {
metadata.getVotes().main {
itemView.ext_votes?.setText(txt(R.string.votes_format, prettyCount(it)))
}
}
if (metadata.fileSize != null) {
itemView.ext_filesize?.isVisible = true

View file

@ -0,0 +1,112 @@
package com.lagradost.cloudstream3.ui.settings.extensions
import android.content.res.ColorStateList
import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.fragment_plugin_details.*
import android.text.format.Formatter.formatFileSize
import com.lagradost.cloudstream3.plugins.VotingApi
import com.lagradost.cloudstream3.plugins.VotingApi.getVoteType
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.plugins.VotingApi.vote
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragment() {
companion object {
private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int {
if (current >= max) return max
if (current >= target) return current
return findClosestBase2(target, current * 2, max)
}
private val iconSizeExact = 50.toPx
private val iconSize by lazy {
findClosestBase2(iconSizeExact, 16, 512)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_plugin_details, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val metadata = data.plugin.second
if (plugin_icon?.setImage(//plugin_icon?.height ?:
metadata.iconUrl?.replace(
"%size%",
"$iconSize"
)?.replace(
"%exact_size%",
"$iconSizeExact"
),
null,
errorImageDrawable = R.drawable.ic_baseline_extension_24
) != true
) {
plugin_icon?.setImageResource(R.drawable.ic_baseline_extension_24)
}
plugin_name?.text = metadata.name
plugin_version?.text = metadata.version.toString()
plugin_description?.text = metadata.description ?: getString(R.string.no_data)
plugin_size?.text = if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize(context, metadata.fileSize)
plugin_author?.text = if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString(", ")
plugin_status?.text = resources.getStringArray(R.array.extension_statuses)[metadata.status]
plugin_types?.text = if ((metadata.tvTypes == null) || metadata.tvTypes.isEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString(", ")
upvote.setOnClickListener {
ioSafe {
metadata.vote(VotingApi.VoteType.UPVOTE).main {
updateVoting(it)
}
}
}
downvote.setOnClickListener {
ioSafe {
metadata.vote(VotingApi.VoteType.DOWNVOTE).main {
updateVoting(it)
}
}
}
ioSafe {
metadata.getVotes().main {
updateVoting(it)
}
}
}
private fun updateVoting(value: Int) {
val metadata = data.plugin.second
plugin_votes.text = value.toString()
when (metadata.getVoteType()) {
VotingApi.VoteType.UPVOTE -> {
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary)
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
}
VotingApi.VoteType.DOWNVOTE -> {
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary)
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
}
VotingApi.VoteType.NONE -> {
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
}
}
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/white"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M15,3L6,3c-0.83,0 -1.54,0.5 -1.84,1.22l-3.02,7.05c-0.09,0.23 -0.14,0.47 -0.14,0.73v2c0,1.1 0.9,2 2,2h6.31l-0.95,4.57 -0.03,0.32c0,0.41 0.17,0.79 0.44,1.06L9.83,23l6.59,-6.59c0.36,-0.36 0.58,-0.86 0.58,-1.41L17,5c0,-1.1 -0.9,-2 -2,-2zM19,3v12h4L23,3h-4z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/white"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M1,21h4L5,9L1,9v12zM23,10c0,-1.1 -0.9,-2 -2,-2h-6.31l0.95,-4.57 0.03,-0.32c0,-0.41 -0.17,-0.79 -0.44,-1.06L14.17,1 7.59,7.59C7.22,7.95 7,8.45 7,9v10c0,1.1 0.9,2 2,2h9c0.83,0 1.54,-0.5 1.84,-1.22l3.02,-7.05c0.09,-0.23 0.14,-0.47 0.14,-0.73v-2z"/>
</vector>

View file

@ -0,0 +1,275 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="?attr/primaryGrayBackground"
android:clipToPadding="false"
android:orientation="vertical"
tools:context=".ui.settings.extensions.PluginDetailsFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:padding="20dp"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:layout_width="50dp"
android:layout_height="50dp"
app:cardCornerRadius="25dp">
<ImageView
android:id="@+id/plugin_icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/plugin_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="normal"
tools:text="Hello world" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:text="@string/extension_description" />
<TextView
android:id="@+id/plugin_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:ellipsize="none"
android:gravity="center_vertical"
android:singleLine="false"
android:textColor="?attr/textColor"
tools:text="Lolem ipsum kek Lolem ipsum kek Lolem ipsum kek Lolem ipsum kek " />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:text="@string/extension_authors" />
<TextView
android:id="@+id/plugin_author"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="false"
android:textColor="?attr/textColor"
tools:text="Lolem ipsum kek" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:text="@string/extension_version" />
<TextView
android:id="@+id/plugin_version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?attr/textColor"
tools:text="Lolem ipsum kek" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:text="@string/extension_status" />
<TextView
android:id="@+id/plugin_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?attr/textColor"
tools:text="Lolem ipsum kek" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:text="@string/extension_size" />
<TextView
android:id="@+id/plugin_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?attr/textColor"
tools:text="Lolem ipsum kek" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:text="@string/extension_types" />
<TextView
android:id="@+id/plugin_types"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="false"
android:textColor="?attr/textColor"
tools:text="Lolem ipsum kek" />
</LinearLayout>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal|center_vertical">
<ImageView
android:id="@+id/downvote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_thumb_down_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/plugin_votes"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/plugin_votes"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="32dp"
android:gravity="center_horizontal|center_vertical"
android:text="0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/upvote"
app:layout_constraintStart_toEndOf="@+id/downvote"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/upvote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_thumb_up_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/plugin_votes"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -73,6 +73,17 @@
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/ext_votes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="5dp"
android:text="10K"
android:textColor="?attr/grayTextColor"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/nsfw_marker"
android:layout_width="wrap_content"
@ -88,6 +99,7 @@
android:id="@+id/sub_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="?attr/grayTextColor"
android:textSize="12sp"
tools:text="https://github.com/..." />

View file

@ -73,6 +73,17 @@
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/ext_votes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="5dp"
android:text="Votes: 0"
android:textColor="?attr/grayTextColor"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/nsfw_marker"
android:layout_width="wrap_content"
@ -88,9 +99,11 @@
android:id="@+id/sub_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="?attr/grayTextColor"
android:textSize="12sp"
tools:text="https://github.com/..." />
</LinearLayout>
<ImageView

View file

@ -257,6 +257,14 @@
<item>Light</item>
</string-array>
<string-array name="extension_statuses">
<item>Down</item>
<!-- "Ok" is usually capitalized as "OK". Ok android studio 🤓-->
<item>Ok</item>
<item>Slow</item>
<item>Beta</item>
</string-array>
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->
<string-array name="subtitles_encoding_list" tools:ignore="TypographyDashes">
<item>@string/automatic</item>

View file

@ -615,4 +615,12 @@
<string name="safe_mode_title">Safe Mode enabled</string>
<string name="safe_mode_description">An unrecoverable crash occurred and we\'ve automatically disabled all extensions, so you can find and remove the extension which is causing trouble.</string>
<string name="safe_mode_crash_info">View crash info</string>
<string name="votes_format" formatted="true">Votes: %s</string>
<string name="extension_description">Description</string>
<string name="extension_version">Version</string>
<string name="extension_status">Status</string>
<string name="extension_size">Size</string>
<string name="extension_authors">Authors</string>
<string name="extension_types">Supported</string>
</resources>