From 8c0e07decbeedc5ad5c6b0413bee90625a9a65a0 Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Sun, 7 Aug 2022 01:43:39 +0200 Subject: [PATCH] Added basic extension management --- .../lagradost/cloudstream3/MainActivity.kt | 15 +- .../cloudstream3/plugins/PluginManager.kt | 149 +++++++++++--- ...positoryParser.kt => RepositoryManager.kt} | 38 +++- .../ui/settings/SettingsFragment.kt | 2 + .../settings/extensions/ExtensionsFragment.kt | 93 +++++++++ .../extensions/ExtensionsViewModel.kt | 31 +++ .../ui/settings/extensions/PluginAdapter.kt | 66 ++++++ .../ui/settings/extensions/PluginsFragment.kt | 83 ++++++++ .../ui/settings/extensions/RepoAdapter.kt | 46 +++++ app/src/main/res/layout/add_repo_input.xml | 104 ++++++++++ .../main/res/layout/fragment_extensions.xml | 34 ++++ app/src/main/res/layout/main_settings.xml | 190 +++++++++--------- app/src/main/res/layout/repository_item.xml | 41 ++++ .../main/res/navigation/mobile_navigation.xml | 42 ++++ app/src/main/res/values/strings.xml | 6 + 15 files changed, 804 insertions(+), 136 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/plugins/{RepositoryParser.kt => RepositoryManager.kt} (63%) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt create mode 100644 app/src/main/res/layout/add_repo_input.xml create mode 100644 app/src/main/res/layout/fragment_extensions.xml create mode 100644 app/src/main/res/layout/repository_item.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d61e8e8e..9b73f2f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -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") diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 4d9ed158..a1e6da60 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -3,26 +3,113 @@ 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 { + return getKey(PLUGINS_KEY) ?: emptyArray() + } + + fun getPluginsLocal(): Array { + return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray() + } + + private val LOCAL_PLUGINS_PATH = Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins" + + private val plugins: MutableMap = LinkedHashMap() private val classLoaders: MutableMap = HashMap() private val failedToLoad: MutableMap = LinkedHashMap() - var loadedPlugins = false + var loadedLocalPlugins = false private val gson = Gson() - fun loadAllPlugins(context: Context) { - val dir = File(PLUGINS_PATH) + + fun maybeLoadPlugin(context: Context, file: File) { + val name = file.name + if (file.extension == "zip" || file.extension == "cs3") { + 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); + } else if (name == "classes.dex" || name.endsWith(".json")) { + // Utils.showToast(String.format("Found extracted plugin file %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true); + } + // rmrf(f); + } + } + + 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) { @@ -30,39 +117,36 @@ object PluginManager { return } } + val sortedPlugins = dir.listFiles() // Always sort plugins alphabetically for reproducible results sortedPlugins?.sortedBy { it.name }?.forEach { file -> - val name = file.name - if (file.extension == "zip" || file.extension == "cs3") { - loadPlugin(context, file) - } 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); - } else if (name == "classes.dex" || name.endsWith(".json")) { - // Utils.showToast(String.format("Found extracted plugin file %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true); - } - // rmrf(f); - } + maybeLoadPlugin(context, file) } - loadedPlugins = true + 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 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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryParser.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt similarity index 63% rename from app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryParser.kt rename to app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index 37a20bd0..cff52871 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryParser.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -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>(REPOSITORIES_KEY) ?: emptyList() + setKey(REPOSITORIES_KEY, currentRepos + repository) + } } private fun write(stream: InputStream, output: OutputStream) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index f86419bc..b0e0c476 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -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 { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt new file mode 100644 index 00000000..d91a2a44 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -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() + + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt new file mode 100644 index 00000000..842a65b2 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt @@ -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>() + val repositories: LiveData> = _repositories + + fun loadRepositories() { + // Crashes weirdly with List + val urls = getKey>(REPOSITORIES_KEY) ?: emptyArray() + _repositories.postValue(urls) + } + + suspend fun getPlugins(repositoryUrl: String): List { + return RepositoryManager.getRepoPlugins(repositoryUrl) ?: emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt new file mode 100644 index 00000000..2a862498 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -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, + val iconClickCallback: PluginAdapter.(plugin: SitePlugin, isDownloaded: Boolean) -> Unit +) : + RecyclerView.Adapter() { + 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 = reloadStoredPlugins() + + fun reloadStoredPlugins(): Array { + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt new file mode 100644 index 00000000..452fd4b0 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt new file mode 100644 index 00000000..92c2e4d1 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt @@ -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, + val clickCallback: (RepositoryData) -> Unit +) : + RecyclerView.Adapter() { + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/add_repo_input.xml b/app/src/main/res/layout/add_repo_input.xml new file mode 100644 index 00000000..c9f41ca3 --- /dev/null +++ b/app/src/main/res/layout/add_repo_input.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_extensions.xml b/app/src/main/res/layout/fragment_extensions.xml new file mode 100644 index 00000000..15bf3833 --- /dev/null +++ b/app/src/main/res/layout/fragment_extensions.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index 8bb41c01..71cad53e 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -1,111 +1,117 @@ + 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:background="?attr/primaryBlackBackground"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="vertical"> + android:id="@+id/settings_profile" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="20dp" + android:visibility="gone" + tools:visibility="visible"> + android:layout_width="50dp" + android:layout_height="50dp" + app:cardCornerRadius="25dp"> + android:id="@+id/settings_profile_pic" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="ContentDescription" /> - - - - - - - - - - - - - - + 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:textSize="18sp" + android:textStyle="normal" + tools:text="Hello world" /> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/repository_item.xml b/app/src/main/res/layout/repository_item.xml new file mode 100644 index 00000000..10ba5336 --- /dev/null +++ b/app/src/main/res/layout/repository_item.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index b3e64c1b..8ee04901 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -128,6 +128,41 @@ app:popEnterAnim="@anim/enter_anim" app:popExitAnim="@anim/exit_anim" /> + + + + + + + + + + Crash reporting What do you want to see Done + Extensions + Add repository + Repository name + Repository url + Plugin Loaded + Plugin Deleted