From a3eef399a9a960b264ef947d15067ed675c965a2 Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:45:13 +0200 Subject: [PATCH] add voting system --- .../cloudstream3/plugins/VotingApi.kt | 86 ++++++ .../cloudstream3/ui/result/EpisodeAdapter.kt | 2 - .../ui/settings/extensions/PluginAdapter.kt | 36 +++ .../extensions/PluginDetailsFragment.kt | 112 +++++++ .../drawable/ic_baseline_thumb_down_24.xml | 5 + .../res/drawable/ic_baseline_thumb_up_24.xml | 5 + .../res/layout/fragment_plugin_details.xml | 275 ++++++++++++++++++ app/src/main/res/layout/repository_item.xml | 12 + .../main/res/layout/repository_item_tv.xml | 13 + app/src/main/res/values/array.xml | 8 + app/src/main/res/values/strings.xml | 8 + 11 files changed, 560 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt create mode 100644 app/src/main/res/drawable/ic_baseline_thumb_down_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_thumb_up_24.xml create mode 100644 app/src/main/res/layout/fragment_plugin_details.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt new file mode 100644 index 00000000..d167ac07 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -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()?.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()?.value + if (res != null) { + setKey("cs3-votes-${transformUrl(repositoryUrl)}/${transformUrl(pluginUrl)}", newType) + } + return res ?: 0 + } + + private data class Result( + val value: Int? + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index a89e8279..3abd827e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 75e7023e..244e37a7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt new file mode 100644 index 00000000..6afb283b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_thumb_down_24.xml b/app/src/main/res/drawable/ic_baseline_thumb_down_24.xml new file mode 100644 index 00000000..fcaea5b2 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_thumb_down_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_thumb_up_24.xml b/app/src/main/res/drawable/ic_baseline_thumb_up_24.xml new file mode 100644 index 00000000..d9aaea4c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_thumb_up_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_plugin_details.xml b/app/src/main/res/layout/fragment_plugin_details.xml new file mode 100644 index 00000000..400e1adf --- /dev/null +++ b/app/src/main/res/layout/fragment_plugin_details.xml @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/repository_item.xml b/app/src/main/res/layout/repository_item.xml index e5872347..e45220b0 100644 --- a/app/src/main/res/layout/repository_item.xml +++ b/app/src/main/res/layout/repository_item.xml @@ -73,6 +73,17 @@ android:visibility="gone" tools:visibility="visible" /> + + diff --git a/app/src/main/res/layout/repository_item_tv.xml b/app/src/main/res/layout/repository_item_tv.xml index aa6346fa..d1445f4c 100644 --- a/app/src/main/res/layout/repository_item_tv.xml +++ b/app/src/main/res/layout/repository_item_tv.xml @@ -73,6 +73,17 @@ android:visibility="gone" tools:visibility="visible" /> + + + Light + + Down + + Ok + Slow + Beta + + @string/automatic diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 11c90d58..d0af69e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -615,4 +615,12 @@ Safe Mode enabled An unrecoverable crash occurred and we\'ve automatically disabled all extensions, so you can find and remove the extension which is causing trouble. View crash info + + Votes: %s + Description + Version + Status + Size + Authors + Supported