updated ui and logic for plugins

This commit is contained in:
reduplicated 2022-08-07 23:11:13 +02:00
parent dd25523bea
commit 4e6bbf3908
11 changed files with 288 additions and 151 deletions

View file

@ -173,6 +173,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_settings_account, R.id.navigation_settings_account,
R.id.navigation_settings_lang, R.id.navigation_settings_lang,
R.id.navigation_settings_general, R.id.navigation_settings_general,
R.id.navigation_settings_extensions,
R.id.navigation_settings_plugins,
).contains(destination.id) ).contains(destination.id)
val landscape = when (resources.configuration.orientation) { val landscape = when (resources.configuration.orientation) {
@ -422,9 +424,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this) app.initClient(this)
PluginManager.updateAllOnlinePlugins(applicationContext) PluginManager.updateAllOnlinePlugins(this)
PluginManager.loadAllLocalPlugins(applicationContext) PluginManager.loadAllLocalPlugins(this)
PluginManager.loadAllOnlinePlugins(applicationContext) PluginManager.loadAllOnlinePlugins(this)
// ioSafe { // ioSafe {
// val plugins = // val plugins =

View file

@ -8,6 +8,7 @@ import android.content.res.Resources
import android.os.Environment import android.os.Environment
import android.widget.Toast import android.widget.Toast
import android.app.Activity import android.app.Activity
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
@ -52,33 +53,31 @@ object PluginManager {
// Prevent multiple writes at once // Prevent multiple writes at once
val lock = Mutex() val lock = Mutex()
const val TAG = "PluginManager"
/** /**
* Store data about the plugin for fetching later * Store data about the plugin for fetching later
* */ * */
private fun setPluginData(data: PluginData) { private suspend fun setPluginData(data: PluginData) {
ioSafe { lock.withLock {
lock.withLock { if (data.isOnline) {
if (data.isOnline) { val plugins = getPluginsOnline()
val plugins = getPluginsOnline() setKey(PLUGINS_KEY, plugins + data)
setKey(PLUGINS_KEY, plugins + data) } else {
} else { val plugins = getPluginsLocal()
val plugins = getPluginsLocal() setKey(PLUGINS_KEY_LOCAL, plugins + data)
setKey(PLUGINS_KEY_LOCAL, plugins + data)
}
} }
} }
} }
private fun deletePluginData(data: PluginData) { private suspend fun deletePluginData(data: PluginData) {
ioSafe { lock.withLock {
lock.withLock { if (data.isOnline) {
if (data.isOnline) { val plugins = getPluginsOnline().filter { it.url != data.url }
val plugins = getPluginsOnline().filter { it.url != data.url } setKey(PLUGINS_KEY, plugins)
setKey(PLUGINS_KEY, plugins) } else {
} else { val plugins = getPluginsLocal().filter { it.filePath != data.filePath }
val plugins = getPluginsLocal().filter { it.filePath != data.filePath } setKey(PLUGINS_KEY_LOCAL, plugins + data)
setKey(PLUGINS_KEY_LOCAL, plugins + data)
}
} }
} }
} }
@ -105,11 +104,11 @@ object PluginManager {
private var loadedLocalPlugins = false private var loadedLocalPlugins = false
private val gson = Gson() private val gson = Gson()
private fun maybeLoadPlugin(context: Context, file: File) { private suspend fun maybeLoadPlugin(activity: Activity, file: File) {
val name = file.name val name = file.name
if (file.extension == "zip" || file.extension == "cs3") { if (file.extension == "zip" || file.extension == "cs3") {
loadPlugin( loadPlugin(
context, activity,
file, file,
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET) PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
) )
@ -119,7 +118,7 @@ object PluginManager {
/** /**
* Needs to be run before other plugin loading because plugin loading can not be overwritten * Needs to be run before other plugin loading because plugin loading can not be overwritten
**/ **/
fun updateAllOnlinePlugins(context: Context) { fun updateAllOnlinePlugins(activity: Activity) {
val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray() val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
val onlinePlugins = urls.toList().apmap { val onlinePlugins = urls.toList().apmap {
@ -136,28 +135,28 @@ object PluginManager {
} }
}.flatten() }.flatten()
println("Outdated plugins: $outdatedPlugins") Log.i(TAG, "Outdated plugins: $outdatedPlugins")
outdatedPlugins.apmap { outdatedPlugins.apmap {
downloadAndLoadPlugin( downloadAndLoadPlugin(
context, activity,
it.second.second.url, it.second.second.url,
it.first.internalName, it.first.internalName,
it.second.first it.second.first
) )
} }
println("Plugin update done!") Log.i(TAG, "Plugin update done!")
} }
fun loadAllOnlinePlugins(context: Context) { fun loadAllOnlinePlugins(activity: Activity) {
File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name } File(activity.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name }
?.forEach { file -> ?.apmap { file ->
maybeLoadPlugin(context, file) maybeLoadPlugin(activity, file)
} }
} }
fun loadAllLocalPlugins(context: Context) { fun loadAllLocalPlugins(activity: Activity) {
val dir = File(LOCAL_PLUGINS_PATH) val dir = File(LOCAL_PLUGINS_PATH)
removeKey(PLUGINS_KEY_LOCAL) removeKey(PLUGINS_KEY_LOCAL)
@ -172,8 +171,8 @@ object PluginManager {
val sortedPlugins = dir.listFiles() val sortedPlugins = dir.listFiles()
// Always sort plugins alphabetically for reproducible results // Always sort plugins alphabetically for reproducible results
sortedPlugins?.sortedBy { it.name }?.forEach { file -> sortedPlugins?.sortedBy { it.name }?.apmap { file ->
maybeLoadPlugin(context, file) maybeLoadPlugin(activity, file)
} }
loadedLocalPlugins = true loadedLocalPlugins = true
@ -182,14 +181,14 @@ object PluginManager {
/** /**
* @return True if successful, false if not * @return True if successful, false if not
* */ * */
private fun loadPlugin(context: Context, file: File, data: PluginData): Boolean { private suspend fun loadPlugin(activity: Activity, file: File, data: PluginData): Boolean {
val fileName = file.nameWithoutExtension val fileName = file.nameWithoutExtension
val filePath = file.absolutePath val filePath = file.absolutePath
println("Loading plugin: $data") Log.i(TAG, "Loading plugin: $data")
//logger.info("Loading plugin: " + fileName); //logger.info("Loading plugin: " + fileName);
return try { return try {
val loader = PathClassLoader(filePath, context.classLoader) val loader = PathClassLoader(filePath, activity.classLoader)
var manifest: Plugin.Manifest var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream -> loader.getResourceAsStream("manifest.json").use { stream ->
if (stream == null) { if (stream == null) {
@ -211,14 +210,15 @@ object PluginManager {
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?> loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
val pluginInstance: Plugin = val pluginInstance: Plugin =
pluginClass.newInstance() as Plugin pluginClass.newInstance() as Plugin
if (plugins.containsKey(filePath)) {
println("Plugin with name $name already exists")
return true
}
// Sets with the proper version // Sets with the proper version
setPluginData(data.copy(version = version)) setPluginData(data.copy(version = version))
if (plugins.containsKey(filePath)) {
Log.i(TAG, "Plugin with name $name already exists")
return true
}
pluginInstance.__filename = fileName pluginInstance.__filename = fileName
if (pluginInstance.needsResources) { if (pluginInstance.needsResources) {
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
@ -228,21 +228,21 @@ object PluginManager {
addAssetPath.invoke(assets, file.absolutePath) addAssetPath.invoke(assets, file.absolutePath)
pluginInstance.resources = Resources( pluginInstance.resources = Resources(
assets, assets,
context.resources.displayMetrics, activity.resources.displayMetrics,
context.resources.configuration activity.resources.configuration
) )
} }
plugins[filePath] = pluginInstance plugins[filePath] = pluginInstance
classLoaders[loader] = pluginInstance classLoaders[loader] = pluginInstance
pluginInstance.load(context) pluginInstance.load(activity)
println("Loaded plugin ${data.internalName} successfully") Log.i(TAG, "Loaded plugin ${data.internalName} successfully")
true true
} catch (e: Throwable) { } catch (e: Throwable) {
failedToLoad[file] = e failedToLoad[file] = e
e.printStackTrace() e.printStackTrace()
showToast( showToast(
context as Activity, activity,
context.getString(R.string.plugin_load_fail).format(fileName), activity.getString(R.string.plugin_load_fail).format(fileName),
Toast.LENGTH_LONG Toast.LENGTH_LONG
) )
false false
@ -250,7 +250,7 @@ object PluginManager {
} }
suspend fun downloadAndLoadPlugin( suspend fun downloadAndLoadPlugin(
context: Context, activity: Activity,
pluginUrl: String, pluginUrl: String,
internalName: String, internalName: String,
repositoryUrl: String repositoryUrl: String
@ -260,21 +260,28 @@ object PluginManager {
true true
) + "." + repositoryUrl.hashCode()) // Guaranteed unique ) + "." + repositoryUrl.hashCode()) // Guaranteed unique
val fileName = (sanitizeFilename(internalName, true) + "." + internalName.hashCode()) val fileName = (sanitizeFilename(internalName, true) + "." + internalName.hashCode())
println("Downloading plugin: $pluginUrl to $folderName/$fileName") Log.i(TAG, "Downloading plugin: $pluginUrl to $folderName/$fileName")
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names // The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
val file = downloadPluginToFile(context, pluginUrl, fileName, folderName) val file = downloadPluginToFile(activity, pluginUrl, fileName, folderName)
return loadPlugin( return loadPlugin(
context, activity,
file ?: return false, file ?: return false,
PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET) PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET)
) )
} }
fun deletePlugin(context: Context, pluginUrl: String, name: String): Boolean { suspend fun deletePlugin(pluginUrl: String): Boolean {
val data = getPluginsOnline() val data = getPluginsOnline()
.firstOrNull { it.url == pluginUrl } .firstOrNull { it.url == pluginUrl }
?: return false ?: return false
deletePluginData(data) return try {
return File(data.filePath).delete() if (File(data.filePath).delete()) {
deletePluginData(data)
return true
}
false
} catch (e: Exception) {
false
}
} }
} }

View file

@ -11,7 +11,6 @@ import android.view.ViewGroup
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -24,6 +23,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.main_settings.* import kotlinx.android.synthetic.main.main_settings.*
import kotlinx.android.synthetic.main.settings_title_top.* import kotlinx.android.synthetic.main.settings_title_top.*
import kotlinx.android.synthetic.main.standard_toolbar.*
import java.io.File import java.io.File
class SettingsFragment : Fragment() { class SettingsFragment : Fragment() {
@ -41,7 +41,19 @@ class SettingsFragment : Fragment() {
} }
} }
fun PreferenceFragmentCompat?.setUpToolbar(@StringRes title: Int) { fun Fragment?.setUpToolbar(title: String) {
if (this == null) return
settings_toolbar?.apply {
setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener {
activity?.onBackPressed()
}
}
context.fixPaddingStatusbar(settings_toolbar)
}
fun Fragment?.setUpToolbar(@StringRes title: Int) {
if (this == null) return if (this == null) return
settings_toolbar?.apply { settings_toolbar?.apply {
setTitle(title) setTitle(title)
@ -138,7 +150,10 @@ class SettingsFragment : Fragment() {
Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui), Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui),
Pair(settings_lang, R.id.action_navigation_settings_to_navigation_settings_lang), Pair(settings_lang, R.id.action_navigation_settings_to_navigation_settings_lang),
Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates), Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates),
Pair(settings_extensions, R.id.action_navigation_settings_to_navigation_settings_extensions), Pair(
settings_extensions,
R.id.action_navigation_settings_to_navigation_settings_extensions
),
).forEach { (view, navigationId) -> ).forEach { (view, navigationId) ->
view?.apply { view?.apply {
setOnClickListener { setOnClickListener {

View file

@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -37,7 +38,9 @@ class ExtensionsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(extensions_root) //context?.fixPaddingStatusbar(extensions_root)
setUpToolbar(R.string.extensions)
repo_recycler_view?.adapter = RepoAdapter(emptyArray(), { repo_recycler_view?.adapter = RepoAdapter(emptyArray(), {
findNavController().navigate( findNavController().navigate(
@ -94,7 +97,5 @@ class ExtensionsFragment : Fragment() {
extensionViewModel.loadRepositories() extensionViewModel.loadRepositories()
} }
} }

View file

@ -5,8 +5,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.plugins.SitePlugin
data class RepositoryData( data class RepositoryData(
@JsonProperty("name") val name: String, @JsonProperty("name") val name: String,
@ -24,8 +22,4 @@ class ExtensionsViewModel : ViewModel() {
val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray() val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
_repositories.postValue(urls) _repositories.postValue(urls)
} }
suspend fun getPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>> {
return RepositoryManager.getRepoPlugins(repositoryUrl) ?: emptyList()
}
} }

View file

@ -4,18 +4,29 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.plugins.PluginData import com.lagradost.cloudstream3.plugins.PluginData
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.SitePlugin import com.lagradost.cloudstream3.plugins.SitePlugin
import com.lagradost.cloudstream3.ui.result.ActorAdaptor
import com.lagradost.cloudstream3.ui.result.DiffCallback
import com.lagradost.cloudstream3.ui.result.UiText
import kotlinx.android.synthetic.main.repository_item.view.* import kotlinx.android.synthetic.main.repository_item.view.*
data class PluginViewData(
val plugin: Plugin,
val isDownloaded: Boolean,
)
class PluginAdapter( class PluginAdapter(
var plugins: List<Pair<String, SitePlugin>>, val iconClickCallback: (Plugin) -> Unit
val iconClickCallback: PluginAdapter.(repositoryUrl: String, plugin: SitePlugin, isDownloaded: Boolean) -> Unit
) : ) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val plugins: MutableList<PluginViewData> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PluginViewHolder( return PluginViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.repository_item, parent, false) LayoutInflater.from(parent.context).inflate(R.layout.repository_item, parent, false)
@ -23,10 +34,9 @@ class PluginAdapter(
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val (repositoryUrl, plugin) = plugins[position]
when (holder) { when (holder) {
is PluginViewHolder -> { is PluginViewHolder -> {
holder.bind(repositoryUrl, plugin) holder.bind(plugins[position])
} }
} }
} }
@ -35,34 +45,61 @@ class PluginAdapter(
return plugins.size return plugins.size
} }
fun updateList(newList: List<PluginViewData>) {
val diffResult = DiffUtil.calculateDiff(
PluginDiffCallback(this.plugins, newList)
)
plugins.clear()
plugins.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
/*
private var storedPlugins: Array<PluginData> = reloadStoredPlugins() private var storedPlugins: Array<PluginData> = reloadStoredPlugins()
fun reloadStoredPlugins(): Array<PluginData> { private fun reloadStoredPlugins(): Array<PluginData> {
return PluginManager.getPluginsOnline().also { storedPlugins = it } return PluginManager.getPluginsOnline().also { storedPlugins = it }
} }*/
inner class PluginViewHolder(itemView: View) : inner class PluginViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(itemView) {
fun bind( fun bind(
repositoryUrl: String, data: PluginViewData,
plugin: SitePlugin,
) { ) {
val isDownloaded = storedPlugins.any { it.url == plugin.url } val metadata = data.plugin.second
val drawableInt = if (isDownloaded) val drawableInt = if (data.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
itemView.nsfw_marker?.isVisible = plugin.isAdult == true itemView.nsfw_marker?.isVisible = metadata.isAdult == true
itemView.action_button?.setImageResource(drawableInt) itemView.action_button?.setImageResource(drawableInt)
itemView.action_button?.setOnClickListener { itemView.action_button?.setOnClickListener {
iconClickCallback.invoke(this@PluginAdapter, repositoryUrl, plugin, isDownloaded) iconClickCallback.invoke(data.plugin)
} }
itemView.main_text?.text = plugin.name itemView.main_text?.text = metadata.name
itemView.sub_text?.text = plugin.description itemView.sub_text?.text = metadata.description
} }
} }
}
class PluginDiffCallback(
private val oldList: List<PluginViewData>,
private val newList: List<PluginViewData>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].plugin.second.internalName == newList[newItemPosition].plugin.second.internalName && oldList[oldItemPosition].plugin.first == newList[newItemPosition].plugin.first
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
} }

View file

@ -9,7 +9,9 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
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
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -27,53 +29,31 @@ class PluginsFragment : Fragment() {
return inflater.inflate(R.layout.fragment_extensions, container, false) return inflater.inflate(R.layout.fragment_extensions, container, false)
} }
private val extensionViewModel: ExtensionsViewModel by activityViewModels() private val pluginViewModel: PluginsViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(extensions_root)
val name = arguments?.getString(PLUGINS_BUNDLE_NAME) val name = arguments?.getString(PLUGINS_BUNDLE_NAME)
val url = arguments?.getString(PLUGINS_BUNDLE_URL) val url = arguments?.getString(PLUGINS_BUNDLE_URL)
if (url == null) { if (url == null) {
activity?.onBackPressed() activity?.onBackPressed()
return return
} }
ioSafe { setUpToolbar(name ?: "Unknown")
val plugins = extensionViewModel.getPlugins(url)
main {
repo_recycler_view?.adapter =
PluginAdapter(plugins) { repositoryUrl, plugin, isDownloaded ->
ioSafe {
val (success, message) = if (isDownloaded) {
PluginManager.deletePlugin(
view.context,
plugin.url,
plugin.name
) to R.string.plugin_deleted
} else {
PluginManager.downloadAndLoadPlugin(
view.context,
plugin.url,
plugin.name,
repositoryUrl
) to R.string.plugin_loaded
}
println("Success: $success") repo_recycler_view?.adapter =
if (success) { PluginAdapter {
main { pluginViewModel.handlePluginAction(activity, url, it)
showToast(activity, message, Toast.LENGTH_SHORT)
this@PluginAdapter.reloadStoredPlugins()
// Dirty and needs a fix
repo_recycler_view?.adapter?.notifyDataSetChanged()
}
}
}
}
} }
observe(pluginViewModel.plugins) {
(repo_recycler_view?.adapter as? PluginAdapter?)?.updateList(it)
} }
pluginViewModel.updatePluginList(url)
} }
companion object { companion object {

View file

@ -0,0 +1,102 @@
package com.lagradost.cloudstream3.ui.settings.extensions
import android.app.Activity
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.plugins.PluginData
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.plugins.SitePlugin
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import kotlinx.coroutines.launch
typealias Plugin = Pair<String, SitePlugin>
class PluginsViewModel : ViewModel() {
private val _plugins = MutableLiveData<List<PluginViewData>>()
val plugins: LiveData<List<PluginViewData>> = _plugins
private val repositoryCache: MutableMap<String, List<Plugin>> = mutableMapOf()
companion object {
const val TAG = "PLG"
}
private suspend fun getPlugins(
repositoryUrl: String,
canUseCache: Boolean = true
): List<Plugin> {
Log.i(TAG, "getPlugins = $repositoryUrl")
if (canUseCache && repositoryCache.containsKey(repositoryUrl)) {
repositoryCache[repositoryUrl]?.let {
return it
}
}
return RepositoryManager.getRepoPlugins(repositoryUrl)
?.also { repositoryCache[repositoryUrl] = it } ?: emptyList()
}
private fun getStoredPlugins(): Array<PluginData> {
return PluginManager.getPluginsOnline()
}
private fun getDownloads(): Set<String> {
return getStoredPlugins().map { it.internalName }.toSet()
}
private fun isDownloaded(plugin: Plugin, data: Set<String>? = null): Boolean {
return (data ?: getDownloads()).contains(plugin.second.internalName)
}
fun handlePluginAction(activity: Activity?, repositoryUrl: String, plugin: Plugin) = ioSafe {
Log.i(TAG, "handlePluginAction = $repositoryUrl, $plugin")
if (activity == null) return@ioSafe
val (repo, metadata) = plugin
val (success, message) = if (isDownloaded(plugin)) {
PluginManager.deletePlugin(
metadata.url,
) to R.string.plugin_deleted
} else {
PluginManager.downloadAndLoadPlugin(
activity,
metadata.url,
metadata.name,
repo
) to R.string.plugin_loaded
}
runOnMainThread {
if (success)
showToast(activity, message, Toast.LENGTH_SHORT)
else
showToast(activity, R.string.error, Toast.LENGTH_SHORT)
}
if (success)
updatePluginListPrivate(repositoryUrl)
}
private suspend fun updatePluginListPrivate(repositoryUrl: String) {
val stored = getDownloads()
val plugins = getPlugins(repositoryUrl)
val list = plugins.map { plugin ->
PluginViewData(plugin, isDownloaded(plugin, stored))
}
_plugins.postValue(list)
}
fun updatePluginList(repositoryUrl: String) = viewModelScope.launch {
Log.i(TAG, "updatePluginList = $repositoryUrl")
updatePluginListPrivate(repositoryUrl)
}
}

View file

@ -1,34 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/extensions_root" android:id="@+id/extensions_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView <include layout="@layout/standard_toolbar" />
<androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/repository_item"
android:id="@+id/repo_recycler_view" android:id="@+id/repo_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/add_repo_button" android:id="@+id/add_repo_button"
style="@style/ExtendedFloatingActionButton" style="@style/ExtendedFloatingActionButton"
android:text="@string/add_repository" android:text="@string/add_repository"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
app:icon="@drawable/ic_baseline_add_24" app:icon="@drawable/ic_baseline_add_24"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -9,22 +9,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout <include layout="@layout/standard_toolbar" />
android:background="@android:color/transparent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_toolbar"
android:paddingTop="@dimen/navbar_height"
tools:title="Overlord"
android:background="?attr/primaryGrayBackground"
app:navigationIconTint="?attr/iconColor"
app:titleTextColor="?attr/textColor"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<!-- Required ViewGroup for PreferenceFragmentCompat --> <!-- Required ViewGroup for PreferenceFragmentCompat -->
<FrameLayout <FrameLayout

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout android:background="@android:color/transparent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_toolbar"
android:paddingTop="@dimen/navbar_height"
tools:title="Overlord"
android:background="?attr/primaryGrayBackground"
app:navigationIconTint="?attr/iconColor"
app:titleTextColor="?attr/textColor"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>