mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
updated ui and logic for plugins
This commit is contained in:
parent
dd25523bea
commit
4e6bbf3908
11 changed files with 288 additions and 151 deletions
|
@ -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 =
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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]
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
19
app/src/main/res/layout/standard_toolbar.xml
Normal file
19
app/src/main/res/layout/standard_toolbar.xml
Normal 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>
|
Loading…
Reference in a new issue