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
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 {

View file

@ -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(

View file

@ -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
)
}
}

View file

@ -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 }

View file

@ -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

View file

@ -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()) {

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.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()
// }
// }
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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(
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()
@ -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)

View file

@ -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"

View file

@ -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