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 21933eb8..9fa0f717 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -9,22 +9,18 @@ import android.widget.Toast import android.app.Activity import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* 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.CommonActivity.showToast -import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.removePluginMapping -import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.extractorApis import kotlinx.coroutines.sync.Mutex @@ -45,7 +41,24 @@ data class PluginData( @JsonProperty("isOnline") val isOnline: Boolean, @JsonProperty("filePath") val filePath: String, @JsonProperty("version") val version: Int, -) +) { + fun toSitePlugin(): SitePlugin { + return SitePlugin( + this.filePath, + PROVIDER_STATUS_OK, + maxOf(1, version), + 1, + internalName, + internalName, + emptyList(), + File(this.filePath).name, + null, + null, + null, + null + ) + } +} // This is used as a placeholder / not set version const val PLUGIN_VERSION_NOT_SET = Int.MIN_VALUE @@ -74,18 +87,28 @@ object PluginManager { } } - private suspend fun deletePluginData(data: PluginData) { + private suspend fun deletePluginData(data: PluginData?) { + if (data == null) return 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) + setKey(PLUGINS_KEY_LOCAL, plugins) } } } + suspend fun deleteRepositoryData(repositoryPath: String) { + lock.withLock { + val plugins = getPluginsOnline().filter { + !it.filePath.contains(repositoryPath) + } + setKey(PLUGINS_KEY, plugins) + } + } + fun getPluginsOnline(): Array { return getKey(PLUGINS_KEY) ?: emptyArray() } @@ -346,10 +369,14 @@ object PluginManager { ) } - suspend fun deletePlugin(pluginUrl: String): Boolean { - val data = getPluginsOnline() - .firstOrNull { it.url == pluginUrl } - ?: return false + /** + * @param isFilePath will treat the pluginUrl as as the filepath instead of url + * */ + suspend fun deletePlugin(pluginIdentifier: String, isFilePath: Boolean): Boolean { + val data = + (if (isFilePath) getPluginsLocal().firstOrNull { it.filePath == pluginIdentifier } + else getPluginsOnline().firstOrNull { it.url == pluginIdentifier }) ?: return false + return try { if (File(data.filePath).delete()) { unloadPlugin(data.filePath) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index b84fb249..98de8a95 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -4,10 +4,12 @@ import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.apmapIndexed import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName +import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsLocal import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -93,9 +95,9 @@ object RepositoryManager { * */ suspend fun getRepoPlugins(repositoryUrl: String): List>? { val repo = parseRepository(repositoryUrl) ?: return null - return repo.pluginLists.apmapIndexed { index, url -> + return repo.pluginLists.apmap { url -> parsePlugins(url).map { - repo.pluginLists[index] to it + repositoryUrl to it } }.flatten() } @@ -150,10 +152,13 @@ object RepositoryManager { setKey(REPOSITORIES_KEY, newRepos) } - File( + val file = File( extensionsDir, getPluginSanitizedFileName(repository.url) - ).delete() + ) + PluginManager.deleteRepositoryData(file.absolutePath) + + file.delete() } private fun write(stream: InputStream, output: OutputStream) { 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 index 51c5696d..2f7e4ae1 100644 --- 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 @@ -59,10 +59,12 @@ class ExtensionsFragment : Fragment() { repo_recycler_view?.adapter = RepoAdapter(false, { 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) - }) + PluginsFragment.newInstance( + it.name, + it.url, + false + ) + ) }, { repo -> // Prompt user before deleting repo main { @@ -120,6 +122,17 @@ class ExtensionsFragment : Fragment() { } } + plugin_storage_appbar?.setOnClickListener { + findNavController().navigate( + R.id.navigation_settings_extensions_to_navigation_settings_plugins, + PluginsFragment.newInstance( + getString(R.string.extensions), + "", + true + ) + ) + } + add_repo_button?.setOnClickListener { val builder = AlertDialog.Builder(context ?: return@setOnClickListener, R.style.AlertDialogCustom) 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 index e02c58bc..02eace5a 100644 --- 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 @@ -12,6 +12,7 @@ import kotlinx.android.synthetic.main.fragment_plugins.* const val PLUGINS_BUNDLE_NAME = "name" const val PLUGINS_BUNDLE_URL = "url" +const val PLUGINS_BUNDLE_LOCAL = "isLocal" class PluginsFragment : Fragment() { override fun onCreateView( @@ -29,6 +30,7 @@ class PluginsFragment : Fragment() { val name = arguments?.getString(PLUGINS_BUNDLE_NAME) val url = arguments?.getString(PLUGINS_BUNDLE_URL) + val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true if (url == null || name == null) { activity?.onBackPressed() @@ -49,21 +51,28 @@ class PluginsFragment : Fragment() { plugin_recycler_view?.adapter = PluginAdapter { - pluginViewModel.handlePluginAction(activity, url, it) + pluginViewModel.handlePluginAction(activity, url, it, isLocal) } observe(pluginViewModel.plugins) { (plugin_recycler_view?.adapter as? PluginAdapter?)?.updateList(it) } - pluginViewModel.updatePluginList(url) + if (isLocal) { + // No download button + settings_toolbar?.menu?.clear() + pluginViewModel.updatePluginListLocal() + } else { + pluginViewModel.updatePluginList(url) + } } companion object { - fun newInstance(name: String, url: String): Bundle { + fun newInstance(name: String, url: String, isLocal: Boolean): Bundle { return Bundle().apply { putString(PLUGINS_BUNDLE_NAME, name) putString(PLUGINS_BUNDLE_URL, url) + putBoolean(PLUGINS_BUNDLE_LOCAL, isLocal) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 25b9fb21..17cf527c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -59,65 +59,76 @@ class PluginsViewModel : ViewModel() { /** * @param viewModel optional, updates the plugins livedata for that viewModel if included * */ - fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = ioSafe { - if (activity == null) return@ioSafe - val stored = getDownloads() - val plugins = getPlugins(repositoryUrl) + fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = + ioSafe { + if (activity == null) return@ioSafe + val stored = getDownloads() + val plugins = getPlugins(repositoryUrl) - plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list -> - main { - showToast( + plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list -> + main { + showToast( + activity, + if (list.isEmpty()) { + txt( + R.string.batch_download_nothing_to_download_format, + txt(R.string.plugin) + ) + } else { + txt( + R.string.batch_download_start_format, + list.size, + txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin) + ) + }, + Toast.LENGTH_SHORT + ) + } + }.apmap { (repo, metadata) -> + PluginManager.downloadAndLoadPlugin( activity, - if (list.isEmpty()) { + metadata.url, + metadata.name, + repo + ) + }.main { list -> + if (list.any { it }) { + showToast( + activity, txt( - R.string.batch_download_nothing_to_download_format, - txt(R.string.plugin) - ) - } else { - txt( - R.string.batch_download_start_format, - list.size, + R.string.batch_download_finish_format, + list.count { it }, txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin) - ) - }, - Toast.LENGTH_SHORT - ) - } - }.apmap { (repo, metadata) -> - PluginManager.downloadAndLoadPlugin( - activity, - metadata.url, - metadata.name, - repo - ) - }.main { list -> - if (list.any { it }) { - showToast( - activity, - txt( - R.string.batch_download_finish_format, - list.count { it }, - txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin) - ), - Toast.LENGTH_SHORT - ) - viewModel?.updatePluginListPrivate(repositoryUrl) - } else if (list.isNotEmpty()) { - showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT) + ), + Toast.LENGTH_SHORT + ) + viewModel?.updatePluginListPrivate(repositoryUrl) + } else if (list.isNotEmpty()) { + showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT) + } } } - } } - fun handlePluginAction(activity: Activity?, repositoryUrl: String, plugin: Plugin) = ioSafe { + /** + * @param isLocal defines if the plugin data is from local data instead of repo + * Will only allow removal of plugins. Used for the local file management. + * */ + fun handlePluginAction( + activity: Activity?, + repositoryUrl: String, + plugin: Plugin, + isLocal: Boolean + ) = ioSafe { Log.i(TAG, "handlePluginAction = $repositoryUrl, $plugin") if (activity == null) return@ioSafe val (repo, metadata) = plugin - val (success, message) = if (isDownloaded(plugin)) { + val (success, message) = if (isDownloaded(plugin) || isLocal) { PluginManager.deletePlugin( metadata.url, + isLocal ) to R.string.plugin_deleted } else { PluginManager.downloadAndLoadPlugin( @@ -136,7 +147,10 @@ class PluginsViewModel : ViewModel() { } if (success) - updatePluginListPrivate(repositoryUrl) + if (isLocal) + updatePluginListLocal() + else + updatePluginListPrivate(repositoryUrl) } private suspend fun updatePluginListPrivate(repositoryUrl: String) { @@ -153,4 +167,20 @@ class PluginsViewModel : ViewModel() { Log.i(TAG, "updatePluginList = $repositoryUrl") updatePluginListPrivate(repositoryUrl) } + + + /** + * Update the list but only with the local data. Used for file management. + * */ + fun updatePluginListLocal() = viewModelScope.launch { + Log.i(TAG, "updatePluginList = local") + + val downloadedPlugins = (PluginManager.getPluginsOnline() + PluginManager.getPluginsLocal()) + .distinctBy { it.filePath } + .map { + PluginViewData("" to it.toSitePlugin(), true) + } + + _plugins.postValue(downloadedPlugins) + } } \ 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 ea27c59e..6a07c086 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -177,6 +177,10 @@ android:name="url" android:defaultValue="@null" app:argType="string" /> +