Compare commits

...

1 commit

Author SHA1 Message Date
firelight
a73e4f5abe
refactor plugin logic and add repository name 2026-06-29 00:30:10 +02:00
12 changed files with 176 additions and 148 deletions

View file

@ -224,17 +224,17 @@ object PluginManager {
// Helper class for updateAllOnlinePluginsAndLoadThem // Helper class for updateAllOnlinePluginsAndLoadThem
data class OnlinePluginData( data class OnlinePluginData(
val savedData: PluginData, val savedData: PluginData,
val onlineData: Pair<String, SitePlugin>, val onlineData: PluginWrapper,
) { ) {
val isOutdated = val isOutdated =
onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE onlineData.plugin.version > savedData.version || onlineData.plugin.version == PLUGIN_VERSION_ALWAYS_UPDATE
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN val isDisabled = onlineData.plugin.status == PROVIDER_STATUS_DOWN
fun validOnlineData(context: Context): Boolean { fun validOnlineData(context: Context): Boolean {
return getPluginPath( return getPluginPath(
context, context,
savedData.internalName, savedData.internalName,
onlineData.first onlineData.repositoryData.url
).absolutePath == savedData.filePath ).absolutePath == savedData.filePath
} }
} }
@ -282,19 +282,19 @@ object PluginManager {
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().amap { val onlinePlugins = urls.toList().amap {
getRepoPlugins(it.url)?.toList() ?: emptyList() getRepoPlugins(it) ?: emptyList()
}.flatten().distinctBy { it.second.url } }.flatten().distinctBy { it.plugin.url }
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
val outdatedPlugins = getPluginsOnline().map { savedData -> val outdatedPlugins = getPluginsOnline().map { savedData ->
onlinePlugins onlinePlugins
.filter { onlineData -> savedData.internalName == onlineData.second.internalName } .filter { onlineData -> savedData.internalName == onlineData.plugin.internalName }
.map { onlineData -> .map { onlineData ->
OnlinePluginData(savedData, onlineData) OnlinePluginData(savedData, onlineData)
}.filter { }.filter {
it.validOnlineData(activity) it.validOnlineData(activity)
} }
}.flatten().distinctBy { it.onlineData.second.url } }.flatten().distinctBy { it.onlineData.plugin.url }
debugPrint { debugPrint {
"Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}" "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}"
@ -309,14 +309,14 @@ object PluginManager {
} else if (pluginData.isOutdated) { } else if (pluginData.isOutdated) {
downloadPlugin( downloadPlugin(
activity, activity,
pluginData.onlineData.second.url, pluginData.onlineData.plugin.url,
pluginData.onlineData.second.fileHash, pluginData.onlineData.plugin.fileHash,
pluginData.savedData.internalName, pluginData.savedData.internalName,
File(pluginData.savedData.filePath), File(pluginData.savedData.filePath),
true true
).let { success -> ).let { success ->
if (success) if (success)
updatedPlugins.add(pluginData.onlineData.second.name) updatedPlugins.add(pluginData.onlineData.plugin.name)
} }
} }
} }
@ -359,15 +359,15 @@ object PluginManager {
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY) val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().amap { val onlinePlugins = urls.toList().amap {
getRepoPlugins(it.url)?.toList() ?: emptyList() getRepoPlugins(it)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url } }.flatten().distinctBy { it.plugin.url }
val providerLang = activity.getApiProviderLangSettings() val providerLang = activity.getApiProviderLangSettings()
//Log.i(TAG, "providerLang => ${providerLang.toJson()}") //Log.i(TAG, "providerLang => ${providerLang.toJson()}")
// Iterate online repos and returns not downloaded plugins // Iterate online repos and returns not downloaded plugins
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData -> val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
val sitePlugin = onlineData.second val sitePlugin = onlineData.plugin
val tvtypes = sitePlugin.tvTypes ?: listOf() val tvtypes = sitePlugin.tvTypes ?: listOf()
//Don't include empty urls //Don't include empty urls
@ -379,7 +379,7 @@ object PluginManager {
} }
//Omit already existing plugins //Omit already existing plugins
if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) { if (getPluginPath(activity, sitePlugin.internalName, onlineData.repositoryData.url).exists()) {
Log.i(TAG, "Skip > ${sitePlugin.internalName}") Log.i(TAG, "Skip > ${sitePlugin.internalName}")
return@mapNotNull null return@mapNotNull null
} }
@ -421,14 +421,14 @@ object PluginManager {
notDownloadedPlugins.amap { pluginData -> notDownloadedPlugins.amap { pluginData ->
downloadPlugin( downloadPlugin(
activity, activity,
pluginData.onlineData.second.url, pluginData.onlineData.plugin.url,
pluginData.onlineData.second.fileHash, pluginData.onlineData.plugin.fileHash,
pluginData.savedData.internalName, pluginData.savedData.internalName,
pluginData.onlineData.first, pluginData.onlineData.repositoryData.url,
!pluginData.isDisabled !pluginData.isDisabled
).let { success -> ).let { success ->
if (success) if (success)
newDownloadPlugins.add(pluginData.onlineData.second.name) newDownloadPlugins.add(pluginData.onlineData.plugin.name)
} }
} }
@ -835,16 +835,16 @@ object PluginManager {
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY) val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().amap { val onlinePlugins = urls.toList().amap {
getRepoPlugins(it.url)?.toList() ?: emptyList() getRepoPlugins(it) ?: emptyList()
}.flatten().distinctBy { it.second.url } }.flatten().distinctBy { it.plugin.url }
val allPlugins = getPluginsOnline().flatMap { savedData -> val allPlugins = getPluginsOnline().flatMap { savedData ->
onlinePlugins onlinePlugins
.filter { it.second.internalName == savedData.internalName } .filter { it.plugin.internalName == savedData.internalName }
.mapNotNull { onlineData -> .mapNotNull { onlineData ->
OnlinePluginData(savedData, onlineData).takeIf { it.validOnlineData(activity) } OnlinePluginData(savedData, onlineData).takeIf { it.validOnlineData(activity) }
} }
}.distinctBy { it.onlineData.second.url } }.distinctBy { it.onlineData.plugin.url }
val updatedPlugins = mutableListOf<String>() val updatedPlugins = mutableListOf<String>()
@ -852,7 +852,7 @@ object PluginManager {
if (pluginData.isDisabled) { if (pluginData.isDisabled) {
Log.e( Log.e(
"PluginManager", "PluginManager",
"Unloading disabled plugin: ${pluginData.onlineData.second.name}" "Unloading disabled plugin: ${pluginData.onlineData.plugin.name}"
) )
unloadPlugin(pluginData.savedData.filePath) unloadPlugin(pluginData.savedData.filePath)
} else { } else {
@ -861,14 +861,14 @@ object PluginManager {
if (downloadPlugin( if (downloadPlugin(
activity, activity,
pluginData.onlineData.second.url, pluginData.onlineData.plugin.url,
pluginData.onlineData.second.fileHash, pluginData.onlineData.plugin.fileHash,
pluginData.savedData.internalName, pluginData.savedData.internalName,
existingFile, existingFile,
true true
) )
) { ) {
updatedPlugins.add(pluginData.onlineData.second.name) updatedPlugins.add(pluginData.onlineData.plugin.name)
} }
} }
}.also { }.also {

View file

@ -75,6 +75,26 @@ data class SitePlugin(
@JsonProperty("fileHash") @SerialName("fileHash") val fileHash: String?, @JsonProperty("fileHash") @SerialName("fileHash") val fileHash: String?,
) )
@Serializable
data class PluginWrapper(
@JsonProperty("repository") @SerialName("repository") val repository: Repository,
@JsonProperty("repositoryData") @SerialName("repositoryData") val repositoryData: RepositoryData,
@JsonProperty("plugin") @SerialName("plugin") val plugin: SitePlugin
) {
companion object {
private val localRepository = Repository("", "", "", 1, emptyList())
private val localRepositoryData = RepositoryData("", "", "")
fun getLocalPluginWrapper(plugin: SitePlugin): PluginWrapper {
return PluginWrapper(
localRepository,
localRepositoryData,
plugin
)
}
}
}
object RepositoryManager { object RepositoryManager {
const val ONLINE_PLUGINS_FOLDER = "Extensions" const val ONLINE_PLUGINS_FOLDER = "Extensions"
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy { val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
@ -135,7 +155,8 @@ object RepositoryManager {
suspend fun parseRepository(url: String): Repository? { suspend fun parseRepository(url: String): Repository? {
return safeAsync { return safeAsync {
// Take manifestVersion and such into account later // Take manifestVersion and such into account later
app.get(convertRawGitUrl(url), cacheTime = 5, cacheUnit = TimeUnit.MINUTES).parsedSafe<Repository>() app.get(convertRawGitUrl(url), cacheTime = 5, cacheUnit = TimeUnit.MINUTES)
.parsedSafe<Repository>()
} }
} }
@ -153,13 +174,14 @@ object RepositoryManager {
/** /**
* Gets all plugins from repositories and pairs them with the repository url * Gets all plugins from repositories and pairs them with the repository url
*/ */
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? { suspend fun getRepoPlugins(repositoryData: RepositoryData): List<PluginWrapper>? {
val repo = parseRepository(repositoryUrl) ?: return null val repo = parseRepository(repositoryData.url) ?: return null
return repo.pluginLists.amap { url -> val list = repo.pluginLists.amap { url ->
parsePlugins(url).map { parsePlugins(url).map {
repositoryUrl to it PluginWrapper(repo, repositoryData, it)
} }
}.flatten() }.flatten()
return list
} }
suspend fun downloadPluginToFile( suspend fun downloadPluginToFile(

View file

@ -114,11 +114,7 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
adapter = RepoAdapter(false, { adapter = RepoAdapter(false, {
findNavController().navigate( findNavController().navigate(
R.id.navigation_settings_extensions_to_navigation_settings_plugins, R.id.navigation_settings_extensions_to_navigation_settings_plugins,
PluginsFragment.newInstance( PluginsFragment.newInstance(it)
it.name,
it.url,
false
)
) )
}, { repo -> }, { repo ->
// Prompt user before deleting repo // Prompt user before deleting repo
@ -156,7 +152,7 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
binding.repoRecyclerView.isVisible = repos.isNotEmpty() binding.repoRecyclerView.isVisible = repos.isNotEmpty()
binding.blankRepoScreen.isVisible = repos.isEmpty() binding.blankRepoScreen.isVisible = repos.isEmpty()
(binding.repoRecyclerView.adapter as? RepoAdapter)?.submitList(repos.toList()) (binding.repoRecyclerView.adapter as? RepoAdapter)?.submitList(repos.toList())
pluginViewModel.updatePluginList(binding.root.context, repos.map { it.url }) pluginViewModel.updatePluginList(binding.root.context, repos.toList())
} }
observeNullable(extensionViewModel.pluginStats) { value -> observeNullable(extensionViewModel.pluginStats) { value ->
@ -185,10 +181,8 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
binding.pluginStorageAppbar.setOnClickListener { binding.pluginStorageAppbar.setOnClickListener {
findNavController().navigate( findNavController().navigate(
R.id.navigation_settings_extensions_to_navigation_settings_plugins, R.id.navigation_settings_extensions_to_navigation_settings_plugins,
PluginsFragment.newInstance( PluginsFragment.newLocalInstance(
getString(R.string.extensions), getString(R.string.extensions),
"",
true
) )
) )
} }
@ -201,9 +195,8 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
) )
setRecycledViewPool(PluginAdapter.sharedPool) setRecycledViewPool(PluginAdapter.sharedPool)
adapter = adapter =
PluginAdapter { PluginAdapter(true) {
val urls = extensionViewModel.repositories.value?.map { repo -> repo.url } val urls = extensionViewModel.repositories.value?.toList() ?: emptyList()
?: emptyList()
pluginViewModel.handlePluginAction(activity, urls, it, false) pluginViewModel.handlePluginAction(activity, urls, it, false)
} }
} }
@ -307,13 +300,12 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
extensionViewModel.loadStats() extensionViewModel.loadStats()
extensionViewModel.loadRepositories() extensionViewModel.loadRepositories()
val plugins = RepositoryManager.getRepoPlugins(url) val plugins = RepositoryManager.getRepoPlugins(newRepo)
if (plugins.isNullOrEmpty()) { if (plugins.isNullOrEmpty()) {
showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG) showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG)
} else { } else {
this@ExtensionsFragment.activity?.addRepositoryDialog( this@ExtensionsFragment.activity?.addRepositoryDialog(
fixedName, newRepo
url,
) )
} }
} }

View file

@ -55,16 +55,16 @@ class ExtensionsViewModel : ViewModel() {
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().amap { val onlinePlugins = urls.toList().amap {
RepositoryManager.getRepoPlugins(it.url)?.toList() ?: emptyList() RepositoryManager.getRepoPlugins(it)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url } }.flatten().distinctBy { it.plugin.url }
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
val outdatedPlugins = getPluginsOnline().map { savedData -> val outdatedPlugins = getPluginsOnline().flatMap { savedData ->
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName } onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.plugin.internalName }
.map { onlineData -> .map { onlineData ->
PluginManager.OnlinePluginData(savedData, onlineData) PluginManager.OnlinePluginData(savedData, onlineData)
} }
}.flatten().distinctBy { it.onlineData.second.url } }.distinctBy { it.onlineData.plugin.url }
val total = onlinePlugins.count() val total = onlinePlugins.count()
val disabled = outdatedPlugins.count { it.isDisabled } val disabled = outdatedPlugins.count { it.isDisabled }

View file

@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginWrapper
import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.BaseDiffCallback
import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.NoStateAdapter
import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.ViewHolderState
@ -34,7 +35,7 @@ import kotlin.math.log10
import kotlin.math.pow import kotlin.math.pow
data class PluginViewData( data class PluginViewData(
val plugin: Plugin, val pluginWrapper: PluginWrapper,
val isDownloaded: Boolean, val isDownloaded: Boolean,
) )
@ -44,9 +45,9 @@ class RepositoryViewHolderState(view: ViewBinding) : ViewHolderState<Any>(view)
} }
class PluginAdapter( class PluginAdapter(
val iconClickCallback: (Plugin) -> Unit val showRepositoryNames: Boolean = false, val iconClickCallback: (PluginWrapper) -> Unit,
) : NoStateAdapter<PluginViewData>(diffCallback = BaseDiffCallback(itemSame = { a, b -> ) : NoStateAdapter<PluginViewData>(diffCallback = BaseDiffCallback(itemSame = { a, b ->
a.plugin.second.internalName == b.plugin.second.internalName && a.plugin.first == b.plugin.first a.pluginWrapper.plugin.internalName == b.pluginWrapper.plugin.internalName && a.pluginWrapper.repositoryData.url == b.pluginWrapper.repositoryData.url
})) { })) {
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> { override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> {
val layout = if (isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item val layout = if (isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item
@ -73,14 +74,22 @@ class PluginAdapter(
val binding = holder.view as? RepositoryItemBinding ?: return val binding = holder.view as? RepositoryItemBinding ?: return
val itemView = holder.itemView val itemView = holder.itemView
val metadata = item.plugin.second val metadata = item.pluginWrapper.plugin
val disabled = metadata.status == PROVIDER_STATUS_DOWN val disabled = metadata.status == PROVIDER_STATUS_DOWN
val name = metadata.name.removeSuffix("Provider") val name = metadata.name.removeSuffix("Provider")
val alpha = if (disabled) 0.6f else 1f val alpha = if (disabled) 0.6f else 1f
val isLocal = !item.plugin.second.url.startsWith("http") val isLocal = !item.pluginWrapper.plugin.url.startsWith("http")
binding.mainText.alpha = alpha binding.mainText.alpha = alpha
binding.subText.alpha = alpha binding.subText.alpha = alpha
binding.repositoryNameText.isVisible = showRepositoryNames
if (showRepositoryNames) {
val name = item.pluginWrapper.repositoryData.name
binding.repositoryNameText.text = name
} else {
binding.repositoryNameText.text = ""
}
val drawableInt = if (item.isDownloaded) val drawableInt = if (item.isDownloaded)
R.drawable.ic_baseline_delete_outline_24 R.drawable.ic_baseline_delete_outline_24
else R.drawable.netflix_download else R.drawable.netflix_download
@ -89,7 +98,7 @@ class PluginAdapter(
binding.actionButton.setImageResource(drawableInt) binding.actionButton.setImageResource(drawableInt)
binding.actionButton.setOnClickListener { binding.actionButton.setOnClickListener {
iconClickCallback.invoke(item.plugin) iconClickCallback.invoke(item.pluginWrapper)
} }
itemView.setOnClickListener { itemView.setOnClickListener {
if (isLocal) return@setOnClickListener if (isLocal) return@setOnClickListener

View file

@ -54,7 +54,7 @@ class PluginDetailsFragment(val data: PluginViewData) : BaseBottomSheetDialogFra
} }
override fun onBindingCreated(binding: FragmentPluginDetailsBinding) { override fun onBindingCreated(binding: FragmentPluginDetailsBinding) {
val metadata = data.plugin.second val metadata = data.pluginWrapper.plugin
binding.apply { binding.apply {
pluginIcon.loadImage(metadata.iconUrl?.replace("%size%", "$iconSize") pluginIcon.loadImage(metadata.iconUrl?.replace("%size%", "$iconSize")
?.replace("%exact_size%", "$iconSizeExact")) { ?.replace("%exact_size%", "$iconSizeExact")) {
@ -135,7 +135,7 @@ class PluginDetailsFragment(val data: PluginViewData) : BaseBottomSheetDialogFra
} }
private fun updateVoting(value: Int) { private fun updateVoting(value: Int) {
val metadata = data.plugin.second val metadata = data.pluginWrapper.plugin
binding?.apply { binding?.apply {
pluginVotes.text = value.toString() pluginVotes.text = value.toString()
if (metadata.hasVoted()) { if (metadata.hasVoted()) {

View file

@ -23,12 +23,13 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setSyst
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
const val PLUGINS_BUNDLE_NAME = "name" const val PLUGINS_BUNDLE_DATA = "data"
const val PLUGINS_BUNDLE_URL = "url"
const val PLUGINS_BUNDLE_LOCAL = "isLocal" const val PLUGINS_BUNDLE_LOCAL = "isLocal"
class PluginsFragment : BaseFragment<FragmentPluginsBinding>( class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
@ -61,24 +62,25 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
} }
} }
val name = arguments?.getString(PLUGINS_BUNDLE_NAME) val repositoryData = arguments?.getString(PLUGINS_BUNDLE_DATA)?.let { data ->
val url = arguments?.getString(PLUGINS_BUNDLE_URL) tryParseJson<RepositoryData>(data)
}
val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true
// download all extensions button // download all extensions button
val downloadAllButton = binding.settingsToolbar.menu?.findItem(R.id.download_all) val downloadAllButton = binding.settingsToolbar.menu?.findItem(R.id.download_all)
if (url == null || name == null) { if (repositoryData == null) {
dispatchBackPressed() dispatchBackPressed()
return return
} }
setToolBarScrollFlags() setToolBarScrollFlags()
setUpToolbar(name) setUpToolbar(repositoryData.name)
binding.settingsToolbar.apply { binding.settingsToolbar.apply {
setOnMenuItemClickListener { menuItem -> setOnMenuItemClickListener { menuItem ->
when (menuItem?.itemId) { when (menuItem?.itemId) {
R.id.download_all -> { R.id.download_all -> {
PluginsViewModel.downloadAll(activity, url, pluginViewModel) PluginsViewModel.downloadAll(activity, repositoryData, pluginViewModel)
} }
R.id.lang_filter -> { R.id.lang_filter -> {
@ -159,7 +161,7 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
setRecycledViewPool(PluginAdapter.sharedPool) setRecycledViewPool(PluginAdapter.sharedPool)
adapter = adapter =
PluginAdapter { PluginAdapter {
pluginViewModel.handlePluginAction(activity, listOf(url), it, isLocal) pluginViewModel.handlePluginAction(activity, listOf(repositoryData), it, isLocal)
} }
} }
@ -183,7 +185,7 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
binding.tvtypesChipsScroll.root.isVisible = false binding.tvtypesChipsScroll.root.isVisible = false
} else { } else {
pluginViewModel.updatePluginList(context, listOf(url)) pluginViewModel.updatePluginList(context, listOf(repositoryData))
binding.tvtypesChipsScroll.root.isVisible = true binding.tvtypesChipsScroll.root.isVisible = true
// not needed for users but may be useful for devs // not needed for users but may be useful for devs
downloadAllButton?.isVisible = BuildConfig.DEBUG downloadAllButton?.isVisible = BuildConfig.DEBUG
@ -204,21 +206,17 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
} }
companion object { companion object {
fun newInstance(name: String, url: String, isLocal: Boolean): Bundle { fun newInstance(repositoryData: RepositoryData): Bundle {
return Bundle().apply { return Bundle().apply {
putString(PLUGINS_BUNDLE_NAME, name) putString(PLUGINS_BUNDLE_DATA, repositoryData.toJson())
putString(PLUGINS_BUNDLE_URL, url) putBoolean(PLUGINS_BUNDLE_LOCAL, false)
putBoolean(PLUGINS_BUNDLE_LOCAL, isLocal) }
}
fun newLocalInstance(name: String): Bundle {
return Bundle().apply {
putString(PLUGINS_BUNDLE_DATA, RepositoryData("", name, "").toJson())
putBoolean(PLUGINS_BUNDLE_LOCAL, true)
} }
} }
// class RepoSearchView(context: Context) : android.widget.SearchView(context) {
// var onActionViewCollapsed = {}
//
// override fun onActionViewCollapsed() {
// onActionViewCollapsed()
// }
// }
} }
} }

View file

@ -17,8 +17,8 @@ import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath
import com.lagradost.cloudstream3.plugins.PluginWrapper
import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.plugins.SitePlugin
import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
@ -26,8 +26,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.Levenshtein import com.lagradost.cloudstream3.utils.Levenshtein
import java.io.File import java.io.File
// String => repository url
typealias Plugin = Pair<String, SitePlugin>
/** /**
* The boolean signifies if the plugin list should be scrolled to the top, used for searching. * The boolean signifies if the plugin list should be scrolled to the top, used for searching.
* */ * */
@ -40,7 +38,7 @@ class PluginsViewModel : ViewModel() {
set(value) { set(value) {
// Also set all the plugin languages for easier filtering // Also set all the plugin languages for easier filtering
value.map { pluginViewData -> value.map { pluginViewData ->
val language = pluginViewData.plugin.second.language?.lowercase() val language = pluginViewData.pluginWrapper.plugin.language?.lowercase()
pluginLanguages.add( pluginLanguages.add(
when { when {
language.isNullOrBlank() -> "none" language.isNullOrBlank() -> "none"
@ -62,7 +60,7 @@ class PluginsViewModel : ViewModel() {
private var currentQuery: String? = null private var currentQuery: String? = null
companion object { companion object {
private val repositoryCache: MutableMap<String, List<Plugin>> = mutableMapOf() private val repositoryCache: MutableMap<String, List<PluginWrapper>> = mutableMapOf()
const val TAG = "PLG" const val TAG = "PLG"
private fun isDownloaded( private fun isDownloaded(
@ -74,32 +72,33 @@ class PluginsViewModel : ViewModel() {
} }
private suspend fun getPlugins( private suspend fun getPlugins(
repositoryUrl: String, repository: RepositoryData,
canUseCache: Boolean = true canUseCache: Boolean = true
): List<Plugin> { ): List<PluginWrapper> {
Log.i(TAG, "getPlugins = $repositoryUrl") Log.i(TAG, "getPlugins = $repository")
if (canUseCache && repositoryCache.containsKey(repositoryUrl)) { if (canUseCache && repositoryCache.containsKey(repository.url)) {
repositoryCache[repositoryUrl]?.let { repositoryCache[repository.url]?.let {
return it return it
} }
} }
return RepositoryManager.getRepoPlugins(repositoryUrl)
?.also { repositoryCache[repositoryUrl] = it } ?: emptyList() return RepositoryManager.getRepoPlugins(repository)
?.also { repositoryCache[repository.url] = it } ?: emptyList()
} }
/** /**
* @param viewModel optional, updates the plugins livedata for that viewModel if included * @param viewModel optional, updates the plugins livedata for that viewModel if included
* */ * */
fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = fun downloadAll(activity: Activity?, repository: RepositoryData, viewModel: PluginsViewModel?) =
ioSafe { ioSafe {
if (activity == null) return@ioSafe if (activity == null) return@ioSafe
val plugins = getPlugins(repositoryUrl) val plugins = getPlugins(repository)
plugins.filter { plugin -> plugins.filter { pluginWrapper ->
!isDownloaded( !isDownloaded(
activity, activity,
plugin.second.internalName, pluginWrapper.plugin.internalName,
repositoryUrl repository.url
) )
}.also { list -> }.also { list ->
main { main {
@ -124,13 +123,13 @@ class PluginsViewModel : ViewModel() {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
) )
} }
}.amap { (repo, metadata) -> }.amap { (_, repo, metadata) ->
PluginManager.downloadPlugin( PluginManager.downloadPlugin(
activity, activity,
metadata.url, metadata.url,
metadata.fileHash, metadata.fileHash,
metadata.internalName, metadata.internalName,
repo, repo.url,
metadata.status != PROVIDER_STATUS_DOWN metadata.status != PROVIDER_STATUS_DOWN
) )
}.main { list -> }.main { list ->
@ -143,7 +142,7 @@ class PluginsViewModel : ViewModel() {
), ),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
) )
viewModel?.updatePluginListPrivate(activity, listOf(repositoryUrl)) viewModel?.updatePluginListPrivate(activity, listOf(repository))
} else if (list.isNotEmpty()) { } else if (list.isNotEmpty()) {
showToast(R.string.download_failed, Toast.LENGTH_SHORT) showToast(R.string.download_failed, Toast.LENGTH_SHORT)
} }
@ -157,32 +156,32 @@ class PluginsViewModel : ViewModel() {
* */ * */
fun handlePluginAction( fun handlePluginAction(
activity: Activity?, activity: Activity?,
repositoryUrls: List<String>, repositoryUrls: List<RepositoryData>,
plugin: Plugin, pluginWrapper: PluginWrapper,
isLocal: Boolean isLocal: Boolean
) = ioSafe { ) = ioSafe {
Log.i(TAG, "handlePluginAction = ${repositoryUrls}, $plugin, $isLocal") Log.i(TAG, "handlePluginAction = ${repositoryUrls}, $pluginWrapper, $isLocal")
if (activity == null) return@ioSafe if (activity == null) return@ioSafe
val (repo, metadata) = plugin val (_, repositoryData, metadata) = pluginWrapper
val file = if (isLocal) File(plugin.second.url) else getPluginPath( val file = if (isLocal) File(pluginWrapper.plugin.url) else getPluginPath(
activity, activity,
plugin.second.internalName, pluginWrapper.plugin.internalName,
plugin.first pluginWrapper.repositoryData.url
) )
val (success, message) = if (file.exists()) { val (success, message) = if (file.exists()) {
PluginManager.deletePlugin(file) to R.string.plugin_deleted PluginManager.deletePlugin(file) to R.string.plugin_deleted
} else { } else {
val isEnabled = plugin.second.status != PROVIDER_STATUS_DOWN val isEnabled = pluginWrapper.plugin.status != PROVIDER_STATUS_DOWN
val message = if (isEnabled) R.string.plugin_loaded else R.string.plugin_downloaded val message = if (isEnabled) R.string.plugin_loaded else R.string.plugin_downloaded
PluginManager.downloadPlugin( PluginManager.downloadPlugin(
activity, activity,
metadata.url, metadata.url,
metadata.fileHash, metadata.fileHash,
metadata.internalName, metadata.internalName,
repo, repositoryData.url,
isEnabled isEnabled
) to message ) to message
} }
@ -201,20 +200,20 @@ class PluginsViewModel : ViewModel() {
updatePluginListPrivate(activity, repositoryUrls) updatePluginListPrivate(activity, repositoryUrls)
} }
private suspend fun updatePluginListPrivate(context: Context, repositoryUrls: List<String>) { private suspend fun updatePluginListPrivate(context: Context, repositories: List<RepositoryData>) {
val isAdult = PreferenceManager.getDefaultSharedPreferences(context) val isAdult = PreferenceManager.getDefaultSharedPreferences(context)
.getStringSet(context.getString(R.string.prefer_media_type_key), emptySet()) .getStringSet(context.getString(R.string.prefer_media_type_key), emptySet())
?.contains(TvType.NSFW.ordinal.toString()) == true ?.contains(TvType.NSFW.ordinal.toString()) == true
val plugins = repositoryUrls.flatMap { repositoryUrl -> val plugins = repositories.flatMap { repositoryUrl ->
getPlugins(repositoryUrl) getPlugins(repositoryUrl)
} }
val list = plugins.filter { val list = plugins.filter {
// Show all non-nsfw plugins or all if nsfw is enabled // Show all non-nsfw plugins or all if nsfw is enabled
it.second.tvTypes?.contains(TvType.NSFW.name) != true || isAdult it.plugin.tvTypes?.contains(TvType.NSFW.name) != true || isAdult
}.map { plugin -> }.map { plugin ->
PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) PluginViewData(plugin, isDownloaded(context, plugin.plugin.internalName, plugin.repositoryData.url))
} }
this.plugins = list this.plugins = list
@ -227,8 +226,8 @@ class PluginsViewModel : ViewModel() {
private fun List<PluginViewData>.filterTvTypes(): List<PluginViewData> { private fun List<PluginViewData>.filterTvTypes(): List<PluginViewData> {
if (tvTypes.isEmpty()) return this if (tvTypes.isEmpty()) return this
return this.filter { return this.filter {
(it.plugin.second.tvTypes?.any { type -> tvTypes.contains(type) } == true) || (it.pluginWrapper.plugin.tvTypes?.any { type -> tvTypes.contains(type) } == true) ||
(tvTypes.contains(TvType.Others.name) && (it.plugin.second.tvTypes (tvTypes.contains(TvType.Others.name) && (it.pluginWrapper.plugin.tvTypes
?: emptyList()).isEmpty()) ?: emptyList()).isEmpty())
} }
} }
@ -236,26 +235,26 @@ class PluginsViewModel : ViewModel() {
private fun List<PluginViewData>.filterLang(): List<PluginViewData> { private fun List<PluginViewData>.filterLang(): List<PluginViewData> {
if (selectedLanguages.isEmpty()) return this // do not filter if (selectedLanguages.isEmpty()) return this // do not filter
return this.filter { return this.filter {
if (it.plugin.second.language == null) { if (it.pluginWrapper.plugin.language == null) {
return@filter selectedLanguages.contains("none") return@filter selectedLanguages.contains("none")
} }
selectedLanguages.contains(it.plugin.second.language?.lowercase()) selectedLanguages.contains(it.pluginWrapper.plugin.language?.lowercase())
} }
} }
private fun List<PluginViewData>.sortByQuery(query: String?): List<PluginViewData> { private fun List<PluginViewData>.sortByQuery(query: String?): List<PluginViewData> {
return if (query.isNullOrBlank()) { return if (query.isNullOrBlank()) {
// Return list to base state if no query // Return list to base state if no query
this.sortedBy { it.plugin.second.name } this.sortedBy { it.pluginWrapper.plugin.name }
} else { } else {
this.mapNotNull { this.mapNotNull {
// Try matching name // Try matching name
val score = Levenshtein.partialRatio( val score = Levenshtein.partialRatio(
it.plugin.second.name.lowercase(), it.pluginWrapper.plugin.name.lowercase(),
query.lowercase() query.lowercase()
).takeIf { score -> score > 80 } ?: ).takeIf { score -> score > 80 } ?:
// Fallback to description, but limit characters to reduce lag // Fallback to description, but limit characters to reduce lag
it.plugin.second.description?.lowercase()?.take(64) it.pluginWrapper.plugin.description?.lowercase()?.take(64)
?.let { description -> ?.let { description ->
Levenshtein.partialRatio( Levenshtein.partialRatio(
description, description,
@ -282,11 +281,11 @@ class PluginsViewModel : ViewModel() {
) )
} }
fun updatePluginList(context: Context?, repositoryUrls: List<String>) = fun updatePluginList(context: Context?, repositories: List<RepositoryData>) =
viewModelScope.launchSafe { viewModelScope.launchSafe {
if (context == null) return@launchSafe if (context == null) return@launchSafe
Log.i(TAG, "updatePluginList = $repositoryUrls") Log.i(TAG, "updatePluginList = $repositories")
updatePluginListPrivate(context, repositoryUrls) updatePluginListPrivate(context, repositories)
} }
fun search(query: String?) { fun search(query: String?) {
@ -305,7 +304,7 @@ class PluginsViewModel : ViewModel() {
val downloadedPlugins = (PluginManager.getPluginsOnline() + PluginManager.getPluginsLocal()) val downloadedPlugins = (PluginManager.getPluginsOnline() + PluginManager.getPluginsLocal())
.distinctBy { it.filePath } .distinctBy { it.filePath }
.map { .map {
PluginViewData("" to it.toSitePlugin(), true) PluginViewData(PluginWrapper.getLocalPluginWrapper(it.toSitePlugin()), true)
} }
plugins = downloadedPlugins plugins = downloadedPlugins

View file

@ -56,7 +56,7 @@ class SetupFragmentExtensions : BaseFragment<FragmentSetupExtensionsBinding>(
if (hasRepos) { if (hasRepos) {
binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, { binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, {
PluginsViewModel.downloadAll(activity, it.url, null) PluginsViewModel.downloadAll(activity, it, null)
}).apply { submitList(repositories.toList()) } }).apply { submitList(repositories.toList()) }
} }
// else { // else {

View file

@ -147,6 +147,7 @@ object AppContextUtils {
text.toSpanned() text.toSpanned()
} }
} }
/** Get channel ID by name */ /** Get channel ID by name */
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildWatchNextProgramUri( private fun buildWatchNextProgramUri(
@ -372,7 +373,8 @@ object AppContextUtils {
val hashSet = HashSet<String>() val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings() val activeLangs = getApiProviderLangSettings()
val hasUniversal = activeLangs.contains(AllLanguagesName) val hasUniversal = activeLangs.contains(AllLanguagesName)
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name }) hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
.map { it.name })
return hashSet return hashSet
} }
@ -463,7 +465,8 @@ object AppContextUtils {
} ?: default } ?: default
val langs = this.getApiProviderLangSettings() val langs = this.getApiProviderLangSettings()
val hasUniversal = langs.contains(AllLanguagesName) val hasUniversal = langs.contains(AllLanguagesName)
val allApis = apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } val allApis =
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
return if (currentPrefMedia.isEmpty()) { return if (currentPrefMedia.isEmpty()) {
allApis allApis
} else { } else {
@ -517,13 +520,12 @@ object AppContextUtils {
fun Activity.loadRepository(url: String) { fun Activity.loadRepository(url: String) {
ioSafe { ioSafe {
val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe
RepositoryManager.addRepository( val data = RepositoryData(
RepositoryData( repo.iconUrl ?: "",
repo.iconUrl ?: "", repo.name,
repo.name, url
url
)
) )
RepositoryManager.addRepository(data)
main { main {
showToast( showToast(
getString(R.string.player_loaded_subtitles, repo.name), getString(R.string.player_loaded_subtitles, repo.name),
@ -531,13 +533,12 @@ object AppContextUtils {
) )
} }
afterRepositoryLoadedEvent.invoke(true) afterRepositoryLoadedEvent.invoke(true)
addRepositoryDialog(repo.name, url) addRepositoryDialog(data)
} }
} }
fun Activity.addRepositoryDialog( fun Activity.addRepositoryDialog(
repositoryName: String, repositoryData: RepositoryData
repositoryURL: String,
) { ) {
val repos = RepositoryManager.getRepositories() val repos = RepositoryManager.getRepositories()
@ -547,9 +548,7 @@ object AppContextUtils {
navigate( navigate(
R.id.global_to_navigation_settings_plugins, R.id.global_to_navigation_settings_plugins,
PluginsFragment.newInstance( PluginsFragment.newInstance(
repositoryName, repositoryData,
repositoryURL,
false,
) )
) )
} }
@ -557,7 +556,7 @@ object AppContextUtils {
runOnUiThread { runOnUiThread {
AlertDialog.Builder(this).apply { AlertDialog.Builder(this).apply {
setTitle(repositoryName) setTitle(repositoryData.name)
setMessage(R.string.download_all_plugins_from_repo) setMessage(R.string.download_all_plugins_from_repo)
setPositiveButton(R.string.open_downloaded_repo) { _, _ -> setPositiveButton(R.string.open_downloaded_repo) { _, _ ->
openAddedRepo() openAddedRepo()
@ -684,7 +683,7 @@ object AppContextUtils {
"$seasonNameShort${rSeason}:$episodeNameShort${rEpisode}" "$seasonNameShort${rSeason}:$episodeNameShort${rEpisode}"
} else if (rEpisode != null) { } else if (rEpisode != null) {
"$episodeNameShort$rEpisode" "$episodeNameShort$rEpisode"
}else null } else null
} }
fun Activity?.loadCache() { fun Activity?.loadCache() {
@ -707,7 +706,7 @@ object AppContextUtils {
fun loadResult( fun loadResult(
url: String, url: String,
apiName: String, apiName: String,
name : String, name: String,
startAction: Int = 0, startAction: Int = 0,
startValue: Int = 0 startValue: Int = 0
) { ) {
@ -717,7 +716,7 @@ object AppContextUtils {
fun FragmentActivity.loadResult( fun FragmentActivity.loadResult(
url: String, url: String,
apiName: String, apiName: String,
name : String, name: String,
startAction: Int = 0, startAction: Int = 0,
startValue: Int = 0 startValue: Int = 0
) { ) {
@ -843,7 +842,8 @@ object AppContextUtils {
} }
fun Context.isUsingMobileData(): Boolean { fun Context.isUsingMobileData(): Boolean {
val connectionManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectionManager =
getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNetwork: Network? = connectionManager.activeNetwork val activeNetwork: Network? = connectionManager.activeNetwork
val networkCapabilities = connectionManager.getNetworkCapabilities(activeNetwork) val networkCapabilities = connectionManager.getNetworkCapabilities(activeNetwork)

View file

@ -53,7 +53,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/primaryBlackBackground" android:background="?attr/primaryBlackBackground"
android:clipToPadding="false" android:clipToPadding="false"
android:layout_marginBottom="80dp"
android:nextFocusLeft="@id/nav_rail_view" android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusUp="@id/tvtypes_chips" android:nextFocusUp="@id/tvtypes_chips"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"

View file

@ -80,8 +80,8 @@
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:textColor="?attr/grayTextColor" android:textColor="?attr/grayTextColor"
tools:text="Votes: 10K"
android:visibility="gone" android:visibility="gone"
tools:text="Votes: 10K"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <TextView
@ -103,6 +103,14 @@
android:textColor="?attr/grayTextColor" android:textColor="?attr/grayTextColor"
android:textSize="12sp" android:textSize="12sp"
tools:text="https://github.com/..." /> tools:text="https://github.com/..." />
<com.google.android.material.button.MaterialButton
android:layout_marginTop="5dp"
tools:visibility="visible"
android:visibility="gone"
android:id="@+id/repository_name_text"
style="@style/SmallBlackButton"
tools:text="Repository name" />
</LinearLayout> </LinearLayout>
<ImageView <ImageView