mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Added basic extension management
This commit is contained in:
parent
e10cb7b0a3
commit
8c0e07decb
15 changed files with 804 additions and 136 deletions
|
@ -39,7 +39,6 @@ import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
|
|||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.network.initClient
|
||||
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||
|
@ -52,16 +51,12 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment
|
|||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
||||
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
|
@ -76,20 +71,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import java.io.File
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.reflect.KClass
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryParser
|
||||
|
||||
|
||||
const val VLC_PACKAGE = "org.videolan.vlc"
|
||||
|
@ -402,7 +391,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
app.initClient(this)
|
||||
|
||||
PluginManager.loadAllPlugins(applicationContext)
|
||||
PluginManager.loadAllLocalPlugins(applicationContext)
|
||||
PluginManager.loadAllOnlinePlugins(applicationContext)
|
||||
|
||||
// ioSafe {
|
||||
// val plugins =
|
||||
// RepositoryParser.getRepoPlugins("https://raw.githubusercontent.com/recloudstream/TestPlugin/master/repo.json")
|
||||
|
|
|
@ -3,40 +3,92 @@ package com.lagradost.cloudstream3.plugins
|
|||
import android.content.Context
|
||||
import dalvik.system.PathClassLoader
|
||||
import com.google.gson.Gson
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Resources
|
||||
import android.os.Environment
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import java.util.*
|
||||
|
||||
// Different keys for local and not since local can be removed at any time without app knowing, hence the local are getting rebuilt on every app start
|
||||
const val PLUGINS_KEY = "PLUGINS_KEY"
|
||||
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
|
||||
|
||||
data class PluginData(
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("url") val url: String?,
|
||||
@JsonProperty("isOnline") val isOnline: Boolean,
|
||||
@JsonProperty("filePath") val filePath: String,
|
||||
)
|
||||
|
||||
object PluginManager {
|
||||
private val PLUGINS_PATH =
|
||||
// Prevent multiple writes at once
|
||||
val lock = Mutex()
|
||||
|
||||
/**
|
||||
* Store data about the plugin for fetching later
|
||||
* */
|
||||
private fun setPluginData(data: PluginData) {
|
||||
ioSafe {
|
||||
lock.withLock {
|
||||
if (data.isOnline) {
|
||||
val plugins = getPluginsOnline()
|
||||
setKey(PLUGINS_KEY, plugins + data)
|
||||
} else {
|
||||
val plugins = getPluginsLocal()
|
||||
setKey(PLUGINS_KEY_LOCAL, plugins + data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deletePluginData(data: PluginData) {
|
||||
ioSafe {
|
||||
lock.withLock {
|
||||
if (data.isOnline) {
|
||||
val plugins = getPluginsOnline().filter { it.url != data.url }
|
||||
setKey(PLUGINS_KEY, plugins)
|
||||
} else {
|
||||
val plugins = getPluginsLocal().filter { it.filePath != data.filePath }
|
||||
setKey(PLUGINS_KEY_LOCAL, plugins + data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPluginsOnline(): Array<PluginData> {
|
||||
return getKey(PLUGINS_KEY) ?: emptyArray()
|
||||
}
|
||||
|
||||
fun getPluginsLocal(): Array<PluginData> {
|
||||
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray()
|
||||
}
|
||||
|
||||
private val LOCAL_PLUGINS_PATH =
|
||||
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins"
|
||||
|
||||
|
||||
private val plugins: MutableMap<String, Plugin> =
|
||||
LinkedHashMap<String, Plugin>()
|
||||
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
|
||||
HashMap<PathClassLoader, Plugin>()
|
||||
private val failedToLoad: MutableMap<File, Any> = LinkedHashMap()
|
||||
var loadedPlugins = false
|
||||
var loadedLocalPlugins = false
|
||||
private val gson = Gson()
|
||||
fun loadAllPlugins(context: Context) {
|
||||
val dir = File(PLUGINS_PATH)
|
||||
if (!dir.exists()) {
|
||||
val res = dir.mkdirs()
|
||||
if (!res) {
|
||||
//logger.error("Failed to create directories!", null);
|
||||
return
|
||||
}
|
||||
}
|
||||
val sortedPlugins = dir.listFiles()
|
||||
// Always sort plugins alphabetically for reproducible results
|
||||
|
||||
sortedPlugins?.sortedBy { it.name }?.forEach { file ->
|
||||
fun maybeLoadPlugin(context: Context, file: File) {
|
||||
val name = file.name
|
||||
if (file.extension == "zip" || file.extension == "cs3") {
|
||||
loadPlugin(context, file)
|
||||
loadPlugin(context, file, PluginData(name, null, false, file.absolutePath))
|
||||
} else if (name != "oat") { // Some roms create this
|
||||
if (file.isDirectory) {
|
||||
// Utils.showToast(String.format("Found directory %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true);
|
||||
|
@ -47,22 +99,54 @@ object PluginManager {
|
|||
}
|
||||
}
|
||||
|
||||
loadedPlugins = true
|
||||
fun loadAllOnlinePlugins(context: Context) {
|
||||
File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name }
|
||||
?.forEach { file ->
|
||||
maybeLoadPlugin(context, file)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAllLocalPlugins(context: Context) {
|
||||
val dir = File(LOCAL_PLUGINS_PATH)
|
||||
removeKey(PLUGINS_KEY_LOCAL)
|
||||
|
||||
if (!dir.exists()) {
|
||||
val res = dir.mkdirs()
|
||||
if (!res) {
|
||||
//logger.error("Failed to create directories!", null);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val sortedPlugins = dir.listFiles()
|
||||
// Always sort plugins alphabetically for reproducible results
|
||||
|
||||
sortedPlugins?.sortedBy { it.name }?.forEach { file ->
|
||||
maybeLoadPlugin(context, file)
|
||||
}
|
||||
|
||||
loadedLocalPlugins = true
|
||||
//if (!PluginManager.failedToLoad.isEmpty())
|
||||
//Utils.showToast("Some plugins failed to load.");
|
||||
}
|
||||
|
||||
fun loadPlugin(context: Context, file: File) {
|
||||
/**
|
||||
* @return True if successful, false if not
|
||||
* */
|
||||
private fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
|
||||
val fileName = file.nameWithoutExtension
|
||||
setPluginData(data)
|
||||
println("Loading plugin: $data")
|
||||
|
||||
//logger.info("Loading plugin: " + fileName);
|
||||
try {
|
||||
return try {
|
||||
val loader = PathClassLoader(file.absolutePath, context.classLoader)
|
||||
var manifest: Plugin.Manifest
|
||||
loader.getResourceAsStream("manifest.json").use { stream ->
|
||||
if (stream == null) {
|
||||
failedToLoad[file] = "No manifest found"
|
||||
//logger.error("Failed to load plugin " + fileName + ": No manifest found", null);
|
||||
return
|
||||
return false
|
||||
}
|
||||
InputStreamReader(stream).use { reader ->
|
||||
manifest = gson.fromJson(
|
||||
|
@ -76,10 +160,10 @@ object PluginManager {
|
|||
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
|
||||
val pluginInstance: Plugin =
|
||||
pluginClass.newInstance() as Plugin
|
||||
if (plugins.containsKey(name)) {
|
||||
// if (plugins.containsKey(name)) {
|
||||
//logger.error("Plugin with name " + name + " already exists", null);
|
||||
return
|
||||
}
|
||||
// return false
|
||||
// }
|
||||
pluginInstance.__filename = fileName
|
||||
if (pluginInstance.needsResources) {
|
||||
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
|
||||
|
@ -96,10 +180,29 @@ object PluginManager {
|
|||
plugins[name] = pluginInstance
|
||||
classLoaders[loader] = pluginInstance
|
||||
pluginInstance.load(context)
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
failedToLoad[file] = e
|
||||
e.printStackTrace()
|
||||
//logger.error("Failed to load plugin " + fileName + ":\n", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadPlugin(context: Context, pluginUrl: String, name: String): Boolean {
|
||||
val file = downloadPluginToFile(context, pluginUrl, name)
|
||||
return loadPlugin(
|
||||
context,
|
||||
file ?: return false,
|
||||
PluginData(name, pluginUrl, true, file.absolutePath)
|
||||
)
|
||||
}
|
||||
|
||||
fun deletePlugin(context: Context, pluginUrl: String, name: String): Boolean {
|
||||
val data = getPluginsOnline()
|
||||
.firstOrNull { it.url == pluginUrl }
|
||||
?: return false
|
||||
deletePluginData(data)
|
||||
return File(data.filePath).delete()
|
||||
}
|
||||
}
|
|
@ -2,10 +2,18 @@ package com.lagradost.cloudstream3.plugins
|
|||
|
||||
import android.content.Context
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
@ -33,7 +41,9 @@ data class SitePlugin(
|
|||
)
|
||||
|
||||
|
||||
object RepositoryParser {
|
||||
object RepositoryManager {
|
||||
const val ONLINE_PLUGINS_FOLDER = "Extensions"
|
||||
|
||||
private suspend fun parseRepository(url: String): Repository? {
|
||||
return suspendSafeApiCall {
|
||||
// Take manifestVersion and such into account later
|
||||
|
@ -56,19 +66,29 @@ object RepositoryParser {
|
|||
}.filterNotNull().flatten()
|
||||
}
|
||||
|
||||
private suspend fun downloadSiteTemp(context: Context, pluginUrl: String, name: String): File? {
|
||||
suspend fun downloadPluginToFile(context: Context, pluginUrl: String, name: String): File? {
|
||||
return suspendSafeApiCall {
|
||||
val dir = context.cacheDir
|
||||
val file = File.createTempFile(name, ".cs3", dir)
|
||||
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
|
||||
if (!extensionsDir.exists())
|
||||
extensionsDir.mkdirs()
|
||||
|
||||
val newFile = File(extensionsDir, "$name.${pluginUrl.hashCode()}.cs3")
|
||||
if (newFile.exists()) return@suspendSafeApiCall newFile
|
||||
newFile.createNewFile()
|
||||
|
||||
val body = app.get(pluginUrl).okhttpResponse.body
|
||||
write(body.byteStream(), file.outputStream())
|
||||
file
|
||||
write(body.byteStream(), newFile.outputStream())
|
||||
newFile
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSiteTemp(context: Context, pluginUrl: String, name: String) {
|
||||
val file = downloadSiteTemp(context, pluginUrl, name)
|
||||
PluginManager.loadPlugin(context, file ?: return)
|
||||
// Don't want to read before we write in another thread
|
||||
private val repoLock = Mutex()
|
||||
suspend fun addRepository(repository: RepositoryData) {
|
||||
repoLock.withLock {
|
||||
val currentRepos = getKey<List<RepositoryData>>(REPOSITORIES_KEY) ?: emptyList()
|
||||
setKey(REPOSITORIES_KEY, currentRepos + repository)
|
||||
}
|
||||
}
|
||||
|
||||
private fun write(stream: InputStream, output: OutputStream) {
|
|
@ -11,6 +11,7 @@ import android.view.ViewGroup
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
|
@ -137,6 +138,7 @@ class SettingsFragment : Fragment() {
|
|||
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_updates, R.id.action_navigation_settings_to_navigation_settings_updates),
|
||||
Pair(settings_extensions, R.id.action_navigation_settings_to_navigation_settings_extensions),
|
||||
).forEach { (view, navigationId) ->
|
||||
view?.apply {
|
||||
setOnClickListener {
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import kotlinx.android.synthetic.main.add_repo_input.*
|
||||
import kotlinx.android.synthetic.main.add_repo_input.apply_btt
|
||||
import kotlinx.android.synthetic.main.add_repo_input.cancel_btt
|
||||
import kotlinx.android.synthetic.main.fragment_extensions.*
|
||||
import kotlinx.android.synthetic.main.stream_input.*
|
||||
|
||||
class ExtensionsFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_extensions, container, false)
|
||||
}
|
||||
|
||||
private val extensionViewModel: ExtensionsViewModel by activityViewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(extensions_root)
|
||||
|
||||
observe(extensionViewModel.repositories) {
|
||||
// Kinda cheap to do this instead of updates
|
||||
repo_recycler_view?.adapter = RepoAdapter(it) {
|
||||
findNavController().navigate(
|
||||
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
|
||||
Bundle().apply {
|
||||
putString(PLUGINS_BUNDLE_NAME, it.name)
|
||||
putString(PLUGINS_BUNDLE_URL, it.url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
add_repo_button?.setOnClickListener {
|
||||
val builder =
|
||||
AlertDialog.Builder(context ?: return@setOnClickListener, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.add_repo_input)
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt(
|
||||
0
|
||||
)?.text?.toString()?.let { copy ->
|
||||
dialog.repo_url_input?.setText(copy)
|
||||
}
|
||||
|
||||
// dialog.text2?.text = provider.name
|
||||
dialog.apply_btt?.setOnClickListener secondListener@{
|
||||
val name = dialog.repo_name_input?.text?.toString()
|
||||
val url = dialog.repo_url_input?.text?.toString()
|
||||
if (url.isNullOrBlank() || name.isNullOrBlank()) {
|
||||
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
|
||||
return@secondListener
|
||||
}
|
||||
|
||||
ioSafe {
|
||||
val newRepo = RepositoryData(name, url)
|
||||
RepositoryManager.addRepository(newRepo)
|
||||
extensionViewModel.loadRepositories()
|
||||
}
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
dialog.cancel_btt?.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extensionViewModel.loadRepositories()
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.plugins.SitePlugin
|
||||
|
||||
data class RepositoryData(
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("url") val url: String
|
||||
)
|
||||
|
||||
const val REPOSITORIES_KEY = "REPOSITORIES_KEY"
|
||||
|
||||
class ExtensionsViewModel : ViewModel() {
|
||||
private val _repositories = MutableLiveData<Array<RepositoryData>>()
|
||||
val repositories: LiveData<Array<RepositoryData>> = _repositories
|
||||
|
||||
fun loadRepositories() {
|
||||
// Crashes weirdly with List<RepositoryData>
|
||||
val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
|
||||
_repositories.postValue(urls)
|
||||
}
|
||||
|
||||
suspend fun getPlugins(repositoryUrl: String): List<SitePlugin> {
|
||||
return RepositoryManager.getRepoPlugins(repositoryUrl) ?: emptyList()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.plugins.PluginData
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.SitePlugin
|
||||
import kotlinx.android.synthetic.main.repository_item.view.*
|
||||
|
||||
class PluginAdapter(
|
||||
var plugins: List<SitePlugin>,
|
||||
val iconClickCallback: PluginAdapter.(plugin: SitePlugin, isDownloaded: Boolean) -> Unit
|
||||
) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return PluginViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.repository_item, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is PluginViewHolder -> {
|
||||
holder.bind(plugins[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return plugins.size
|
||||
}
|
||||
|
||||
private var storedPlugins: Array<PluginData> = reloadStoredPlugins()
|
||||
|
||||
fun reloadStoredPlugins(): Array<PluginData> {
|
||||
return PluginManager.getPluginsOnline().also { storedPlugins = it }
|
||||
}
|
||||
|
||||
inner class PluginViewHolder(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
fun bind(
|
||||
plugin: SitePlugin
|
||||
) {
|
||||
val isDownloaded = storedPlugins.any { it.url == plugin.url }
|
||||
println("ISOWNLOADED $isDownloaded ${storedPlugins.map { it.url }} ||||| ${plugin.url}")
|
||||
|
||||
|
||||
val drawableInt = if (isDownloaded)
|
||||
R.drawable.ic_baseline_delete_outline_24
|
||||
else R.drawable.netflix_download
|
||||
|
||||
itemView.action_button.setImageResource(drawableInt)
|
||||
|
||||
itemView.action_button?.setOnClickListener {
|
||||
iconClickCallback.invoke(this@PluginAdapter, plugin, isDownloaded)
|
||||
}
|
||||
|
||||
itemView.main_text?.text = plugin.name
|
||||
itemView.sub_text?.text = plugin.description
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import kotlinx.android.synthetic.main.fragment_extensions.*
|
||||
|
||||
const val PLUGINS_BUNDLE_NAME = "name"
|
||||
const val PLUGINS_BUNDLE_URL = "url"
|
||||
|
||||
class PluginsFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_extensions, container, false)
|
||||
}
|
||||
|
||||
private val extensionViewModel: ExtensionsViewModel by activityViewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(extensions_root)
|
||||
|
||||
val name = arguments?.getString(PLUGINS_BUNDLE_NAME)
|
||||
val url = arguments?.getString(PLUGINS_BUNDLE_URL)
|
||||
if (url == null) {
|
||||
activity?.onBackPressed()
|
||||
return
|
||||
}
|
||||
|
||||
ioSafe {
|
||||
val plugins = extensionViewModel.getPlugins(url)
|
||||
println("GET PLUGINS $plugins")
|
||||
main {
|
||||
repo_recycler_view?.adapter = PluginAdapter(plugins) { plugin, isDownloaded ->
|
||||
ioSafe {
|
||||
val (success, message) = if (isDownloaded) {
|
||||
PluginManager.deletePlugin(view.context, plugin.url, plugin.name) to R.string.plugin_deleted
|
||||
} else {
|
||||
PluginManager.downloadPlugin(
|
||||
view.context,
|
||||
plugin.url,
|
||||
plugin.name
|
||||
) to R.string.plugin_loaded
|
||||
}
|
||||
|
||||
println("Success: $success")
|
||||
if (success) {
|
||||
main {
|
||||
showToast(activity, message, Toast.LENGTH_SHORT)
|
||||
this@PluginAdapter.reloadStoredPlugins()
|
||||
// Dirty and needs a fix
|
||||
repo_recycler_view?.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(name: String, url: String): Bundle {
|
||||
return Bundle().apply {
|
||||
putString(PLUGINS_BUNDLE_NAME, name)
|
||||
putString(PLUGINS_BUNDLE_URL, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.settings.AccountClickCallback
|
||||
import kotlinx.android.synthetic.main.repository_item.view.*
|
||||
|
||||
class RepoAdapter(
|
||||
private val repositories: Array<RepositoryData>,
|
||||
val clickCallback: (RepositoryData) -> Unit
|
||||
) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return RepoViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.repository_item, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is RepoViewHolder -> {
|
||||
holder.bind(repositories[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return repositories.size
|
||||
}
|
||||
|
||||
inner class RepoViewHolder(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
fun bind(
|
||||
repositoryData: RepositoryData
|
||||
) {
|
||||
itemView.setOnClickListener {
|
||||
clickCallback(repositoryData)
|
||||
}
|
||||
itemView.main_text?.text = repositoryData.name
|
||||
itemView.sub_text?.text = repositoryData.url
|
||||
}
|
||||
}
|
||||
}
|
104
app/src/main/res/layout/add_repo_input.xml
Normal file
104
app/src/main/res/layout/add_repo_input.xml
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:text="@string/add_repository"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text2"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
||||
android:layout_marginBottom="10dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="15sp"
|
||||
android:visibility="gone"
|
||||
tools:text="Gogoanime" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/repo_name_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:hint="@string/repository_name_hint"
|
||||
android:inputType="text"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusDown="@id/site_url_input"
|
||||
android:requiresFadingEdge="vertical"
|
||||
android:textColorHint="?attr/grayTextColor"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/repo_url_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/repository_url_hint"
|
||||
android:inputType="textUri"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
|
||||
android:nextFocusUp="@id/site_name_input"
|
||||
android:nextFocusDown="@id/site_lang_input"
|
||||
android:requiresFadingEdge="vertical"
|
||||
android:textColorHint="?attr/grayTextColor"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/apply_btt_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginTop="-60dp"
|
||||
android:gravity="bottom|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/apply_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/add_repository" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_cancel" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
34
app/src/main/res/layout/fragment_extensions.xml
Normal file
34
app/src/main/res/layout/fragment_extensions.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/repo_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/add_repo_button"
|
||||
style="@style/ExtendedFloatingActionButton"
|
||||
android:text="@string/add_repository"
|
||||
android:textColor="?attr/textColor"
|
||||
app:icon="@drawable/ic_baseline_add_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
@ -3,34 +3,34 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryBlackBackground">
|
||||
|
||||
<ScrollView
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:id="@+id/settings_profile"
|
||||
android:padding="20dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
app:cardCornerRadius="25dp"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp">
|
||||
android:layout_height="50dp"
|
||||
app:cardCornerRadius="25dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/settings_profile_pic"
|
||||
|
@ -41,71 +41,77 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/settings_profile_text"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="normal"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
tools:text="Hello world"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
android:textSize="18sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="Hello world" />
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:nextFocusDown="@id/settings_player"
|
||||
|
||||
<TextView
|
||||
android:id="@+id/settings_general"
|
||||
|
||||
style="@style/SettingsItem"
|
||||
android:nextFocusDown="@id/settings_player"
|
||||
android:text="@string/category_general" />
|
||||
|
||||
<TextView
|
||||
android:nextFocusUp="@id/settings_general"
|
||||
android:nextFocusDown="@id/settings_lang"
|
||||
|
||||
android:id="@+id/settings_player"
|
||||
style="@style/SettingsItem"
|
||||
|
||||
android:nextFocusUp="@id/settings_general"
|
||||
android:nextFocusDown="@id/settings_lang"
|
||||
android:text="@string/category_player" />
|
||||
|
||||
<TextView
|
||||
android:nextFocusUp="@id/settings_player"
|
||||
android:nextFocusDown="@id/settings_ui"
|
||||
|
||||
android:id="@+id/settings_lang"
|
||||
style="@style/SettingsItem"
|
||||
|
||||
android:nextFocusUp="@id/settings_player"
|
||||
android:nextFocusDown="@id/settings_ui"
|
||||
android:text="@string/category_preferred_media_and_lang" />
|
||||
|
||||
<TextView
|
||||
android:nextFocusUp="@id/settings_lang"
|
||||
android:nextFocusDown="@id/settings_updates"
|
||||
|
||||
android:id="@+id/settings_ui"
|
||||
style="@style/SettingsItem"
|
||||
|
||||
android:nextFocusUp="@id/settings_lang"
|
||||
android:nextFocusDown="@id/settings_updates"
|
||||
android:text="@string/category_ui" />
|
||||
|
||||
<TextView
|
||||
android:nextFocusUp="@id/settings_ui"
|
||||
android:nextFocusDown="@id/settings_credits"
|
||||
|
||||
android:id="@+id/settings_updates"
|
||||
style="@style/SettingsItem"
|
||||
|
||||
android:nextFocusUp="@id/settings_ui"
|
||||
android:nextFocusDown="@id/settings_credits"
|
||||
android:text="@string/category_updates" />
|
||||
|
||||
<TextView
|
||||
android:nextFocusUp="@id/settings_updates"
|
||||
|
||||
android:id="@+id/settings_credits"
|
||||
style="@style/SettingsItem"
|
||||
android:nextFocusUp="@id/settings_updates"
|
||||
android:text="@string/category_account" />
|
||||
|
||||
<TextView
|
||||
android:padding="10dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="?attr/textColor"
|
||||
android:text="@string/app_version"
|
||||
android:id="@+id/settings_extensions"
|
||||
style="@style/SettingsItem"
|
||||
android:nextFocusUp="@id/settings_updates"
|
||||
android:text="@string/extensions" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:text="@string/app_version"
|
||||
android:textColor="?attr/textColor" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
41
app/src/main/res/layout/repository_item.xml
Normal file
41
app/src/main/res/layout/repository_item.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/main_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
tools:text="Test repository" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sub_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="12sp"
|
||||
tools:text="https://github.com/..." />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/action_button"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_width="wrap_content"
|
||||
tools:src="@drawable/ic_baseline_add_24"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
</ImageView>
|
||||
|
||||
|
||||
</LinearLayout>
|
|
@ -128,6 +128,41 @@
|
|||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_settings_extensions"
|
||||
android:name="com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment"
|
||||
android:label="@string/title_settings"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim">
|
||||
<action
|
||||
android:id="@+id/navigation_settings_extensions_to_navigation_settings_plugins"
|
||||
app:destination="@id/navigation_settings_plugins"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim">
|
||||
<argument
|
||||
android:name="name"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string" />
|
||||
<argument
|
||||
android:name="url"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string" />
|
||||
</action>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_settings_plugins"
|
||||
android:name="com.lagradost.cloudstream3.ui.settings.extensions.PluginsFragment"
|
||||
android:label="@string/title_settings"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_settings_lang"
|
||||
android:name="com.lagradost.cloudstream3.ui.settings.SettingsLang"
|
||||
|
@ -306,6 +341,13 @@
|
|||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_settings_to_navigation_settings_extensions"
|
||||
app:destination="@id/navigation_settings_extensions"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
|
|
@ -569,4 +569,10 @@
|
|||
<string name="crash_reporting_title">Crash reporting</string>
|
||||
<string name="preferred_media_subtext">What do you want to see</string>
|
||||
<string name="setup_done">Done</string>
|
||||
<string name="extensions">Extensions</string>
|
||||
<string name="add_repository">Add repository</string>
|
||||
<string name="repository_name_hint">Repository name</string>
|
||||
<string name="repository_url_hint">Repository url</string>
|
||||
<string name="plugin_loaded">Plugin Loaded</string>
|
||||
<string name="plugin_deleted">Plugin Deleted</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue