mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-07-13 00:13:17 +00:00
Compare commits
1 commit
master
...
pluginrefa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a73e4f5abe |
12 changed files with 176 additions and 148 deletions
|
|
@ -224,17 +224,17 @@ object PluginManager {
|
|||
// Helper class for updateAllOnlinePluginsAndLoadThem
|
||||
data class OnlinePluginData(
|
||||
val savedData: PluginData,
|
||||
val onlineData: Pair<String, SitePlugin>,
|
||||
val onlineData: PluginWrapper,
|
||||
) {
|
||||
val isOutdated =
|
||||
onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
|
||||
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN
|
||||
onlineData.plugin.version > savedData.version || onlineData.plugin.version == PLUGIN_VERSION_ALWAYS_UPDATE
|
||||
val isDisabled = onlineData.plugin.status == PROVIDER_STATUS_DOWN
|
||||
|
||||
fun validOnlineData(context: Context): Boolean {
|
||||
return getPluginPath(
|
||||
context,
|
||||
savedData.internalName,
|
||||
onlineData.first
|
||||
onlineData.repositoryData.url
|
||||
).absolutePath == savedData.filePath
|
||||
}
|
||||
}
|
||||
|
|
@ -282,19 +282,19 @@ object PluginManager {
|
|||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||
|
||||
val onlinePlugins = urls.toList().amap {
|
||||
getRepoPlugins(it.url)?.toList() ?: emptyList()
|
||||
}.flatten().distinctBy { it.second.url }
|
||||
getRepoPlugins(it) ?: emptyList()
|
||||
}.flatten().distinctBy { it.plugin.url }
|
||||
|
||||
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
|
||||
val outdatedPlugins = getPluginsOnline().map { savedData ->
|
||||
onlinePlugins
|
||||
.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
|
||||
.filter { onlineData -> savedData.internalName == onlineData.plugin.internalName }
|
||||
.map { onlineData ->
|
||||
OnlinePluginData(savedData, onlineData)
|
||||
}.filter {
|
||||
it.validOnlineData(activity)
|
||||
}
|
||||
}.flatten().distinctBy { it.onlineData.second.url }
|
||||
}.flatten().distinctBy { it.onlineData.plugin.url }
|
||||
|
||||
debugPrint {
|
||||
"Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}"
|
||||
|
|
@ -309,14 +309,14 @@ object PluginManager {
|
|||
} else if (pluginData.isOutdated) {
|
||||
downloadPlugin(
|
||||
activity,
|
||||
pluginData.onlineData.second.url,
|
||||
pluginData.onlineData.second.fileHash,
|
||||
pluginData.onlineData.plugin.url,
|
||||
pluginData.onlineData.plugin.fileHash,
|
||||
pluginData.savedData.internalName,
|
||||
File(pluginData.savedData.filePath),
|
||||
true
|
||||
).let { 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)
|
||||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||
val onlinePlugins = urls.toList().amap {
|
||||
getRepoPlugins(it.url)?.toList() ?: emptyList()
|
||||
}.flatten().distinctBy { it.second.url }
|
||||
getRepoPlugins(it)?.toList() ?: emptyList()
|
||||
}.flatten().distinctBy { it.plugin.url }
|
||||
|
||||
val providerLang = activity.getApiProviderLangSettings()
|
||||
//Log.i(TAG, "providerLang => ${providerLang.toJson()}")
|
||||
|
||||
// Iterate online repos and returns not downloaded plugins
|
||||
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
|
||||
val sitePlugin = onlineData.second
|
||||
val sitePlugin = onlineData.plugin
|
||||
val tvtypes = sitePlugin.tvTypes ?: listOf()
|
||||
|
||||
//Don't include empty urls
|
||||
|
|
@ -379,7 +379,7 @@ object PluginManager {
|
|||
}
|
||||
|
||||
//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}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
|
@ -421,14 +421,14 @@ object PluginManager {
|
|||
notDownloadedPlugins.amap { pluginData ->
|
||||
downloadPlugin(
|
||||
activity,
|
||||
pluginData.onlineData.second.url,
|
||||
pluginData.onlineData.second.fileHash,
|
||||
pluginData.onlineData.plugin.url,
|
||||
pluginData.onlineData.plugin.fileHash,
|
||||
pluginData.savedData.internalName,
|
||||
pluginData.onlineData.first,
|
||||
pluginData.onlineData.repositoryData.url,
|
||||
!pluginData.isDisabled
|
||||
).let { 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)
|
||||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||
val onlinePlugins = urls.toList().amap {
|
||||
getRepoPlugins(it.url)?.toList() ?: emptyList()
|
||||
}.flatten().distinctBy { it.second.url }
|
||||
getRepoPlugins(it) ?: emptyList()
|
||||
}.flatten().distinctBy { it.plugin.url }
|
||||
|
||||
val allPlugins = getPluginsOnline().flatMap { savedData ->
|
||||
onlinePlugins
|
||||
.filter { it.second.internalName == savedData.internalName }
|
||||
.filter { it.plugin.internalName == savedData.internalName }
|
||||
.mapNotNull { onlineData ->
|
||||
OnlinePluginData(savedData, onlineData).takeIf { it.validOnlineData(activity) }
|
||||
}
|
||||
}.distinctBy { it.onlineData.second.url }
|
||||
}.distinctBy { it.onlineData.plugin.url }
|
||||
|
||||
val updatedPlugins = mutableListOf<String>()
|
||||
|
||||
|
|
@ -852,7 +852,7 @@ object PluginManager {
|
|||
if (pluginData.isDisabled) {
|
||||
Log.e(
|
||||
"PluginManager",
|
||||
"Unloading disabled plugin: ${pluginData.onlineData.second.name}"
|
||||
"Unloading disabled plugin: ${pluginData.onlineData.plugin.name}"
|
||||
)
|
||||
unloadPlugin(pluginData.savedData.filePath)
|
||||
} else {
|
||||
|
|
@ -861,14 +861,14 @@ object PluginManager {
|
|||
|
||||
if (downloadPlugin(
|
||||
activity,
|
||||
pluginData.onlineData.second.url,
|
||||
pluginData.onlineData.second.fileHash,
|
||||
pluginData.onlineData.plugin.url,
|
||||
pluginData.onlineData.plugin.fileHash,
|
||||
pluginData.savedData.internalName,
|
||||
existingFile,
|
||||
true
|
||||
)
|
||||
) {
|
||||
updatedPlugins.add(pluginData.onlineData.second.name)
|
||||
updatedPlugins.add(pluginData.onlineData.plugin.name)
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,26 @@ data class SitePlugin(
|
|||
@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 {
|
||||
const val ONLINE_PLUGINS_FOLDER = "Extensions"
|
||||
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
|
||||
|
|
@ -135,7 +155,8 @@ object RepositoryManager {
|
|||
suspend fun parseRepository(url: String): Repository? {
|
||||
return safeAsync {
|
||||
// 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
|
||||
*/
|
||||
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
|
||||
val repo = parseRepository(repositoryUrl) ?: return null
|
||||
return repo.pluginLists.amap { url ->
|
||||
suspend fun getRepoPlugins(repositoryData: RepositoryData): List<PluginWrapper>? {
|
||||
val repo = parseRepository(repositoryData.url) ?: return null
|
||||
val list = repo.pluginLists.amap { url ->
|
||||
parsePlugins(url).map {
|
||||
repositoryUrl to it
|
||||
PluginWrapper(repo, repositoryData, it)
|
||||
}
|
||||
}.flatten()
|
||||
return list
|
||||
}
|
||||
|
||||
suspend fun downloadPluginToFile(
|
||||
|
|
|
|||
|
|
@ -114,11 +114,7 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
adapter = RepoAdapter(false, {
|
||||
findNavController().navigate(
|
||||
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
|
||||
PluginsFragment.newInstance(
|
||||
it.name,
|
||||
it.url,
|
||||
false
|
||||
)
|
||||
PluginsFragment.newInstance(it)
|
||||
)
|
||||
}, { repo ->
|
||||
// Prompt user before deleting repo
|
||||
|
|
@ -156,7 +152,7 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
binding.repoRecyclerView.isVisible = repos.isNotEmpty()
|
||||
binding.blankRepoScreen.isVisible = repos.isEmpty()
|
||||
(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 ->
|
||||
|
|
@ -185,10 +181,8 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
binding.pluginStorageAppbar.setOnClickListener {
|
||||
findNavController().navigate(
|
||||
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
|
||||
PluginsFragment.newInstance(
|
||||
PluginsFragment.newLocalInstance(
|
||||
getString(R.string.extensions),
|
||||
"",
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -201,9 +195,8 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
)
|
||||
setRecycledViewPool(PluginAdapter.sharedPool)
|
||||
adapter =
|
||||
PluginAdapter {
|
||||
val urls = extensionViewModel.repositories.value?.map { repo -> repo.url }
|
||||
?: emptyList()
|
||||
PluginAdapter(true) {
|
||||
val urls = extensionViewModel.repositories.value?.toList() ?: emptyList()
|
||||
pluginViewModel.handlePluginAction(activity, urls, it, false)
|
||||
}
|
||||
}
|
||||
|
|
@ -307,13 +300,12 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
extensionViewModel.loadStats()
|
||||
extensionViewModel.loadRepositories()
|
||||
|
||||
val plugins = RepositoryManager.getRepoPlugins(url)
|
||||
val plugins = RepositoryManager.getRepoPlugins(newRepo)
|
||||
if (plugins.isNullOrEmpty()) {
|
||||
showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG)
|
||||
} else {
|
||||
this@ExtensionsFragment.activity?.addRepositoryDialog(
|
||||
fixedName,
|
||||
url,
|
||||
newRepo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,16 +55,16 @@ class ExtensionsViewModel : ViewModel() {
|
|||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||
|
||||
val onlinePlugins = urls.toList().amap {
|
||||
RepositoryManager.getRepoPlugins(it.url)?.toList() ?: emptyList()
|
||||
}.flatten().distinctBy { it.second.url }
|
||||
RepositoryManager.getRepoPlugins(it)?.toList() ?: emptyList()
|
||||
}.flatten().distinctBy { it.plugin.url }
|
||||
|
||||
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
|
||||
val outdatedPlugins = getPluginsOnline().map { savedData ->
|
||||
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
|
||||
val outdatedPlugins = getPluginsOnline().flatMap { savedData ->
|
||||
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.plugin.internalName }
|
||||
.map { onlineData ->
|
||||
PluginManager.OnlinePluginData(savedData, onlineData)
|
||||
}
|
||||
}.flatten().distinctBy { it.onlineData.second.url }
|
||||
}.distinctBy { it.onlineData.plugin.url }
|
||||
|
||||
val total = onlinePlugins.count()
|
||||
val disabled = outdatedPlugins.count { it.isDisabled }
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.R
|
|||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.PluginWrapper
|
||||
import com.lagradost.cloudstream3.ui.BaseDiffCallback
|
||||
import com.lagradost.cloudstream3.ui.NoStateAdapter
|
||||
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||
|
|
@ -34,7 +35,7 @@ import kotlin.math.log10
|
|||
import kotlin.math.pow
|
||||
|
||||
data class PluginViewData(
|
||||
val plugin: Plugin,
|
||||
val pluginWrapper: PluginWrapper,
|
||||
val isDownloaded: Boolean,
|
||||
)
|
||||
|
||||
|
|
@ -44,9 +45,9 @@ class RepositoryViewHolderState(view: ViewBinding) : ViewHolderState<Any>(view)
|
|||
}
|
||||
|
||||
class PluginAdapter(
|
||||
val iconClickCallback: (Plugin) -> Unit
|
||||
val showRepositoryNames: Boolean = false, val iconClickCallback: (PluginWrapper) -> Unit,
|
||||
) : 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> {
|
||||
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 itemView = holder.itemView
|
||||
|
||||
val metadata = item.plugin.second
|
||||
val metadata = item.pluginWrapper.plugin
|
||||
val disabled = metadata.status == PROVIDER_STATUS_DOWN
|
||||
val name = metadata.name.removeSuffix("Provider")
|
||||
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.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)
|
||||
R.drawable.ic_baseline_delete_outline_24
|
||||
else R.drawable.netflix_download
|
||||
|
|
@ -89,7 +98,7 @@ class PluginAdapter(
|
|||
binding.actionButton.setImageResource(drawableInt)
|
||||
|
||||
binding.actionButton.setOnClickListener {
|
||||
iconClickCallback.invoke(item.plugin)
|
||||
iconClickCallback.invoke(item.pluginWrapper)
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
if (isLocal) return@setOnClickListener
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class PluginDetailsFragment(val data: PluginViewData) : BaseBottomSheetDialogFra
|
|||
}
|
||||
|
||||
override fun onBindingCreated(binding: FragmentPluginDetailsBinding) {
|
||||
val metadata = data.plugin.second
|
||||
val metadata = data.pluginWrapper.plugin
|
||||
binding.apply {
|
||||
pluginIcon.loadImage(metadata.iconUrl?.replace("%size%", "$iconSize")
|
||||
?.replace("%exact_size%", "$iconSizeExact")) {
|
||||
|
|
@ -135,7 +135,7 @@ class PluginDetailsFragment(val data: PluginViewData) : BaseBottomSheetDialogFra
|
|||
}
|
||||
|
||||
private fun updateVoting(value: Int) {
|
||||
val metadata = data.plugin.second
|
||||
val metadata = data.pluginWrapper.plugin
|
||||
binding?.apply {
|
||||
pluginVotes.text = value.toString()
|
||||
if (metadata.hasVoted()) {
|
||||
|
|
|
|||
|
|
@ -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.setUpToolbar
|
||||
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.SubtitleHelper.getNameNextToFlagEmoji
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
|
||||
const val PLUGINS_BUNDLE_NAME = "name"
|
||||
const val PLUGINS_BUNDLE_URL = "url"
|
||||
const val PLUGINS_BUNDLE_DATA = "data"
|
||||
const val PLUGINS_BUNDLE_LOCAL = "isLocal"
|
||||
|
||||
class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
||||
|
|
@ -61,24 +62,25 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
|||
}
|
||||
}
|
||||
|
||||
val name = arguments?.getString(PLUGINS_BUNDLE_NAME)
|
||||
val url = arguments?.getString(PLUGINS_BUNDLE_URL)
|
||||
val repositoryData = arguments?.getString(PLUGINS_BUNDLE_DATA)?.let { data ->
|
||||
tryParseJson<RepositoryData>(data)
|
||||
}
|
||||
val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true
|
||||
// download all extensions button
|
||||
val downloadAllButton = binding.settingsToolbar.menu?.findItem(R.id.download_all)
|
||||
|
||||
if (url == null || name == null) {
|
||||
if (repositoryData == null) {
|
||||
dispatchBackPressed()
|
||||
return
|
||||
}
|
||||
|
||||
setToolBarScrollFlags()
|
||||
setUpToolbar(name)
|
||||
setUpToolbar(repositoryData.name)
|
||||
binding.settingsToolbar.apply {
|
||||
setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem?.itemId) {
|
||||
R.id.download_all -> {
|
||||
PluginsViewModel.downloadAll(activity, url, pluginViewModel)
|
||||
PluginsViewModel.downloadAll(activity, repositoryData, pluginViewModel)
|
||||
}
|
||||
|
||||
R.id.lang_filter -> {
|
||||
|
|
@ -159,7 +161,7 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
|||
setRecycledViewPool(PluginAdapter.sharedPool)
|
||||
adapter =
|
||||
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
|
||||
} else {
|
||||
pluginViewModel.updatePluginList(context, listOf(url))
|
||||
pluginViewModel.updatePluginList(context, listOf(repositoryData))
|
||||
binding.tvtypesChipsScroll.root.isVisible = true
|
||||
// not needed for users but may be useful for devs
|
||||
downloadAllButton?.isVisible = BuildConfig.DEBUG
|
||||
|
|
@ -204,21 +206,17 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(name: String, url: String, isLocal: Boolean): Bundle {
|
||||
fun newInstance(repositoryData: RepositoryData): Bundle {
|
||||
return Bundle().apply {
|
||||
putString(PLUGINS_BUNDLE_NAME, name)
|
||||
putString(PLUGINS_BUNDLE_URL, url)
|
||||
putBoolean(PLUGINS_BUNDLE_LOCAL, isLocal)
|
||||
putString(PLUGINS_BUNDLE_DATA, repositoryData.toJson())
|
||||
putBoolean(PLUGINS_BUNDLE_LOCAL, false)
|
||||
}
|
||||
}
|
||||
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()
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,8 @@ import com.lagradost.cloudstream3.amap
|
|||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath
|
||||
import com.lagradost.cloudstream3.plugins.PluginWrapper
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.plugins.SitePlugin
|
||||
import com.lagradost.cloudstream3.utils.txt
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
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 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.
|
||||
* */
|
||||
|
|
@ -40,7 +38,7 @@ class PluginsViewModel : ViewModel() {
|
|||
set(value) {
|
||||
// Also set all the plugin languages for easier filtering
|
||||
value.map { pluginViewData ->
|
||||
val language = pluginViewData.plugin.second.language?.lowercase()
|
||||
val language = pluginViewData.pluginWrapper.plugin.language?.lowercase()
|
||||
pluginLanguages.add(
|
||||
when {
|
||||
language.isNullOrBlank() -> "none"
|
||||
|
|
@ -62,7 +60,7 @@ class PluginsViewModel : ViewModel() {
|
|||
private var currentQuery: String? = null
|
||||
|
||||
companion object {
|
||||
private val repositoryCache: MutableMap<String, List<Plugin>> = mutableMapOf()
|
||||
private val repositoryCache: MutableMap<String, List<PluginWrapper>> = mutableMapOf()
|
||||
const val TAG = "PLG"
|
||||
|
||||
private fun isDownloaded(
|
||||
|
|
@ -74,32 +72,33 @@ class PluginsViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private suspend fun getPlugins(
|
||||
repositoryUrl: String,
|
||||
repository: RepositoryData,
|
||||
canUseCache: Boolean = true
|
||||
): List<Plugin> {
|
||||
Log.i(TAG, "getPlugins = $repositoryUrl")
|
||||
if (canUseCache && repositoryCache.containsKey(repositoryUrl)) {
|
||||
repositoryCache[repositoryUrl]?.let {
|
||||
): List<PluginWrapper> {
|
||||
Log.i(TAG, "getPlugins = $repository")
|
||||
if (canUseCache && repositoryCache.containsKey(repository.url)) {
|
||||
repositoryCache[repository.url]?.let {
|
||||
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
|
||||
* */
|
||||
fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) =
|
||||
fun downloadAll(activity: Activity?, repository: RepositoryData, viewModel: PluginsViewModel?) =
|
||||
ioSafe {
|
||||
if (activity == null) return@ioSafe
|
||||
val plugins = getPlugins(repositoryUrl)
|
||||
val plugins = getPlugins(repository)
|
||||
|
||||
plugins.filter { plugin ->
|
||||
plugins.filter { pluginWrapper ->
|
||||
!isDownloaded(
|
||||
activity,
|
||||
plugin.second.internalName,
|
||||
repositoryUrl
|
||||
pluginWrapper.plugin.internalName,
|
||||
repository.url
|
||||
)
|
||||
}.also { list ->
|
||||
main {
|
||||
|
|
@ -124,13 +123,13 @@ class PluginsViewModel : ViewModel() {
|
|||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}.amap { (repo, metadata) ->
|
||||
}.amap { (_, repo, metadata) ->
|
||||
PluginManager.downloadPlugin(
|
||||
activity,
|
||||
metadata.url,
|
||||
metadata.fileHash,
|
||||
metadata.internalName,
|
||||
repo,
|
||||
repo.url,
|
||||
metadata.status != PROVIDER_STATUS_DOWN
|
||||
)
|
||||
}.main { list ->
|
||||
|
|
@ -143,7 +142,7 @@ class PluginsViewModel : ViewModel() {
|
|||
),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
viewModel?.updatePluginListPrivate(activity, listOf(repositoryUrl))
|
||||
viewModel?.updatePluginListPrivate(activity, listOf(repository))
|
||||
} else if (list.isNotEmpty()) {
|
||||
showToast(R.string.download_failed, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
|
@ -157,32 +156,32 @@ class PluginsViewModel : ViewModel() {
|
|||
* */
|
||||
fun handlePluginAction(
|
||||
activity: Activity?,
|
||||
repositoryUrls: List<String>,
|
||||
plugin: Plugin,
|
||||
repositoryUrls: List<RepositoryData>,
|
||||
pluginWrapper: PluginWrapper,
|
||||
isLocal: Boolean
|
||||
) = ioSafe {
|
||||
Log.i(TAG, "handlePluginAction = ${repositoryUrls}, $plugin, $isLocal")
|
||||
Log.i(TAG, "handlePluginAction = ${repositoryUrls}, $pluginWrapper, $isLocal")
|
||||
|
||||
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,
|
||||
plugin.second.internalName,
|
||||
plugin.first
|
||||
pluginWrapper.plugin.internalName,
|
||||
pluginWrapper.repositoryData.url
|
||||
)
|
||||
|
||||
val (success, message) = if (file.exists()) {
|
||||
PluginManager.deletePlugin(file) to R.string.plugin_deleted
|
||||
} 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
|
||||
PluginManager.downloadPlugin(
|
||||
activity,
|
||||
metadata.url,
|
||||
metadata.fileHash,
|
||||
metadata.internalName,
|
||||
repo,
|
||||
repositoryData.url,
|
||||
isEnabled
|
||||
) to message
|
||||
}
|
||||
|
|
@ -201,20 +200,20 @@ class PluginsViewModel : ViewModel() {
|
|||
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)
|
||||
.getStringSet(context.getString(R.string.prefer_media_type_key), emptySet())
|
||||
?.contains(TvType.NSFW.ordinal.toString()) == true
|
||||
|
||||
val plugins = repositoryUrls.flatMap { repositoryUrl ->
|
||||
val plugins = repositories.flatMap { repositoryUrl ->
|
||||
getPlugins(repositoryUrl)
|
||||
}
|
||||
|
||||
val list = plugins.filter {
|
||||
// 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 ->
|
||||
PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first))
|
||||
PluginViewData(plugin, isDownloaded(context, plugin.plugin.internalName, plugin.repositoryData.url))
|
||||
}
|
||||
|
||||
this.plugins = list
|
||||
|
|
@ -227,8 +226,8 @@ class PluginsViewModel : ViewModel() {
|
|||
private fun List<PluginViewData>.filterTvTypes(): List<PluginViewData> {
|
||||
if (tvTypes.isEmpty()) return this
|
||||
return this.filter {
|
||||
(it.plugin.second.tvTypes?.any { type -> tvTypes.contains(type) } == true) ||
|
||||
(tvTypes.contains(TvType.Others.name) && (it.plugin.second.tvTypes
|
||||
(it.pluginWrapper.plugin.tvTypes?.any { type -> tvTypes.contains(type) } == true) ||
|
||||
(tvTypes.contains(TvType.Others.name) && (it.pluginWrapper.plugin.tvTypes
|
||||
?: emptyList()).isEmpty())
|
||||
}
|
||||
}
|
||||
|
|
@ -236,26 +235,26 @@ class PluginsViewModel : ViewModel() {
|
|||
private fun List<PluginViewData>.filterLang(): List<PluginViewData> {
|
||||
if (selectedLanguages.isEmpty()) return this // do not filter
|
||||
return this.filter {
|
||||
if (it.plugin.second.language == null) {
|
||||
if (it.pluginWrapper.plugin.language == null) {
|
||||
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> {
|
||||
return if (query.isNullOrBlank()) {
|
||||
// Return list to base state if no query
|
||||
this.sortedBy { it.plugin.second.name }
|
||||
this.sortedBy { it.pluginWrapper.plugin.name }
|
||||
} else {
|
||||
this.mapNotNull {
|
||||
// Try matching name
|
||||
val score = Levenshtein.partialRatio(
|
||||
it.plugin.second.name.lowercase(),
|
||||
it.pluginWrapper.plugin.name.lowercase(),
|
||||
query.lowercase()
|
||||
).takeIf { score -> score > 80 } ?:
|
||||
// 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 ->
|
||||
Levenshtein.partialRatio(
|
||||
description,
|
||||
|
|
@ -282,11 +281,11 @@ class PluginsViewModel : ViewModel() {
|
|||
)
|
||||
}
|
||||
|
||||
fun updatePluginList(context: Context?, repositoryUrls: List<String>) =
|
||||
fun updatePluginList(context: Context?, repositories: List<RepositoryData>) =
|
||||
viewModelScope.launchSafe {
|
||||
if (context == null) return@launchSafe
|
||||
Log.i(TAG, "updatePluginList = $repositoryUrls")
|
||||
updatePluginListPrivate(context, repositoryUrls)
|
||||
Log.i(TAG, "updatePluginList = $repositories")
|
||||
updatePluginListPrivate(context, repositories)
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
|
|
@ -305,7 +304,7 @@ class PluginsViewModel : ViewModel() {
|
|||
val downloadedPlugins = (PluginManager.getPluginsOnline() + PluginManager.getPluginsLocal())
|
||||
.distinctBy { it.filePath }
|
||||
.map {
|
||||
PluginViewData("" to it.toSitePlugin(), true)
|
||||
PluginViewData(PluginWrapper.getLocalPluginWrapper(it.toSitePlugin()), true)
|
||||
}
|
||||
|
||||
plugins = downloadedPlugins
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class SetupFragmentExtensions : BaseFragment<FragmentSetupExtensionsBinding>(
|
|||
|
||||
if (hasRepos) {
|
||||
binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, {
|
||||
PluginsViewModel.downloadAll(activity, it.url, null)
|
||||
PluginsViewModel.downloadAll(activity, it, null)
|
||||
}).apply { submitList(repositories.toList()) }
|
||||
}
|
||||
// else {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ object AppContextUtils {
|
|||
text.toSpanned()
|
||||
}
|
||||
}
|
||||
|
||||
/** Get channel ID by name */
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildWatchNextProgramUri(
|
||||
|
|
@ -372,7 +373,8 @@ object AppContextUtils {
|
|||
val hashSet = HashSet<String>()
|
||||
val activeLangs = getApiProviderLangSettings()
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -463,7 +465,8 @@ object AppContextUtils {
|
|||
} ?: default
|
||||
val langs = this.getApiProviderLangSettings()
|
||||
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()) {
|
||||
allApis
|
||||
} else {
|
||||
|
|
@ -517,13 +520,12 @@ object AppContextUtils {
|
|||
fun Activity.loadRepository(url: String) {
|
||||
ioSafe {
|
||||
val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe
|
||||
RepositoryManager.addRepository(
|
||||
RepositoryData(
|
||||
repo.iconUrl ?: "",
|
||||
repo.name,
|
||||
url
|
||||
)
|
||||
val data = RepositoryData(
|
||||
repo.iconUrl ?: "",
|
||||
repo.name,
|
||||
url
|
||||
)
|
||||
RepositoryManager.addRepository(data)
|
||||
main {
|
||||
showToast(
|
||||
getString(R.string.player_loaded_subtitles, repo.name),
|
||||
|
|
@ -531,13 +533,12 @@ object AppContextUtils {
|
|||
)
|
||||
}
|
||||
afterRepositoryLoadedEvent.invoke(true)
|
||||
addRepositoryDialog(repo.name, url)
|
||||
addRepositoryDialog(data)
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.addRepositoryDialog(
|
||||
repositoryName: String,
|
||||
repositoryURL: String,
|
||||
repositoryData: RepositoryData
|
||||
) {
|
||||
val repos = RepositoryManager.getRepositories()
|
||||
|
||||
|
|
@ -547,9 +548,7 @@ object AppContextUtils {
|
|||
navigate(
|
||||
R.id.global_to_navigation_settings_plugins,
|
||||
PluginsFragment.newInstance(
|
||||
repositoryName,
|
||||
repositoryURL,
|
||||
false,
|
||||
repositoryData,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -557,7 +556,7 @@ object AppContextUtils {
|
|||
|
||||
runOnUiThread {
|
||||
AlertDialog.Builder(this).apply {
|
||||
setTitle(repositoryName)
|
||||
setTitle(repositoryData.name)
|
||||
setMessage(R.string.download_all_plugins_from_repo)
|
||||
setPositiveButton(R.string.open_downloaded_repo) { _, _ ->
|
||||
openAddedRepo()
|
||||
|
|
@ -684,7 +683,7 @@ object AppContextUtils {
|
|||
"$seasonNameShort${rSeason}:$episodeNameShort${rEpisode}"
|
||||
} else if (rEpisode != null) {
|
||||
"$episodeNameShort$rEpisode"
|
||||
}else null
|
||||
} else null
|
||||
}
|
||||
|
||||
fun Activity?.loadCache() {
|
||||
|
|
@ -707,7 +706,7 @@ object AppContextUtils {
|
|||
fun loadResult(
|
||||
url: String,
|
||||
apiName: String,
|
||||
name : String,
|
||||
name: String,
|
||||
startAction: Int = 0,
|
||||
startValue: Int = 0
|
||||
) {
|
||||
|
|
@ -717,7 +716,7 @@ object AppContextUtils {
|
|||
fun FragmentActivity.loadResult(
|
||||
url: String,
|
||||
apiName: String,
|
||||
name : String,
|
||||
name: String,
|
||||
startAction: Int = 0,
|
||||
startValue: Int = 0
|
||||
) {
|
||||
|
|
@ -843,7 +842,8 @@ object AppContextUtils {
|
|||
}
|
||||
|
||||
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) {
|
||||
val activeNetwork: Network? = connectionManager.activeNetwork
|
||||
val networkCapabilities = connectionManager.getNetworkCapabilities(activeNetwork)
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:clipToPadding="false"
|
||||
|
||||
android:layout_marginBottom="80dp"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusUp="@id/tvtypes_chips"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@
|
|||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
tools:text="Votes: 10K"
|
||||
android:visibility="gone"
|
||||
tools:text="Votes: 10K"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
|
|
@ -103,6 +103,14 @@
|
|||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="12sp"
|
||||
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>
|
||||
|
||||
<ImageView
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue