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_lang, | ||||
|             R.id.navigation_settings_general, | ||||
|             R.id.navigation_settings_extensions, | ||||
|             R.id.navigation_settings_plugins, | ||||
|         ).contains(destination.id) | ||||
| 
 | ||||
|         val landscape = when (resources.configuration.orientation) { | ||||
|  | @ -422,9 +424,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         app.initClient(this) | ||||
| 
 | ||||
|         PluginManager.updateAllOnlinePlugins(applicationContext) | ||||
|         PluginManager.loadAllLocalPlugins(applicationContext) | ||||
|         PluginManager.loadAllOnlinePlugins(applicationContext) | ||||
|         PluginManager.updateAllOnlinePlugins(this) | ||||
|         PluginManager.loadAllLocalPlugins(this) | ||||
|         PluginManager.loadAllOnlinePlugins(this) | ||||
| 
 | ||||
| //        ioSafe { | ||||
| //            val plugins = | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import android.content.res.Resources | |||
| import android.os.Environment | ||||
| import android.widget.Toast | ||||
| import android.app.Activity | ||||
| import android.util.Log | ||||
| import com.fasterxml.jackson.annotation.JsonProperty | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey | ||||
|  | @ -52,33 +53,31 @@ object PluginManager { | |||
|     // Prevent multiple writes at once | ||||
|     val lock = Mutex() | ||||
| 
 | ||||
|     const val TAG = "PluginManager" | ||||
| 
 | ||||
|     /** | ||||
|      * 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 suspend fun setPluginData(data: PluginData) { | ||||
|         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) | ||||
|                 } | ||||
|     private suspend fun deletePluginData(data: PluginData) { | ||||
|         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) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -105,11 +104,11 @@ object PluginManager { | |||
|     private var loadedLocalPlugins = false | ||||
|     private val gson = Gson() | ||||
| 
 | ||||
|     private fun maybeLoadPlugin(context: Context, file: File) { | ||||
|     private suspend fun maybeLoadPlugin(activity: Activity, file: File) { | ||||
|         val name = file.name | ||||
|         if (file.extension == "zip" || file.extension == "cs3") { | ||||
|             loadPlugin( | ||||
|                 context, | ||||
|                 activity, | ||||
|                 file, | ||||
|                 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 | ||||
|      **/ | ||||
|     fun updateAllOnlinePlugins(context: Context) { | ||||
|     fun updateAllOnlinePlugins(activity: Activity) { | ||||
|         val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray() | ||||
| 
 | ||||
|         val onlinePlugins = urls.toList().apmap { | ||||
|  | @ -136,28 +135,28 @@ object PluginManager { | |||
|                 } | ||||
|         }.flatten() | ||||
| 
 | ||||
|         println("Outdated plugins: $outdatedPlugins") | ||||
|         Log.i(TAG, "Outdated plugins: $outdatedPlugins") | ||||
| 
 | ||||
|         outdatedPlugins.apmap { | ||||
|             downloadAndLoadPlugin( | ||||
|                 context, | ||||
|                 activity, | ||||
|                 it.second.second.url, | ||||
|                 it.first.internalName, | ||||
|                 it.second.first | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         println("Plugin update done!") | ||||
|         Log.i(TAG, "Plugin update done!") | ||||
|     } | ||||
| 
 | ||||
|     fun loadAllOnlinePlugins(context: Context) { | ||||
|         File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name } | ||||
|             ?.forEach { file -> | ||||
|                 maybeLoadPlugin(context, file) | ||||
|     fun loadAllOnlinePlugins(activity: Activity) { | ||||
|         File(activity.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name } | ||||
|             ?.apmap { file -> | ||||
|                 maybeLoadPlugin(activity, file) | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     fun loadAllLocalPlugins(context: Context) { | ||||
|     fun loadAllLocalPlugins(activity: Activity) { | ||||
|         val dir = File(LOCAL_PLUGINS_PATH) | ||||
|         removeKey(PLUGINS_KEY_LOCAL) | ||||
| 
 | ||||
|  | @ -172,8 +171,8 @@ object PluginManager { | |||
|         val sortedPlugins = dir.listFiles() | ||||
|         // Always sort plugins alphabetically for reproducible results | ||||
| 
 | ||||
|         sortedPlugins?.sortedBy { it.name }?.forEach { file -> | ||||
|             maybeLoadPlugin(context, file) | ||||
|         sortedPlugins?.sortedBy { it.name }?.apmap { file -> | ||||
|             maybeLoadPlugin(activity, file) | ||||
|         } | ||||
| 
 | ||||
|         loadedLocalPlugins = true | ||||
|  | @ -182,14 +181,14 @@ object PluginManager { | |||
|     /** | ||||
|      * @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 filePath = file.absolutePath | ||||
|         println("Loading plugin: $data") | ||||
|         Log.i(TAG, "Loading plugin: $data") | ||||
| 
 | ||||
|         //logger.info("Loading plugin: " + fileName); | ||||
|         return try { | ||||
|             val loader = PathClassLoader(filePath, context.classLoader) | ||||
|             val loader = PathClassLoader(filePath, activity.classLoader) | ||||
|             var manifest: Plugin.Manifest | ||||
|             loader.getResourceAsStream("manifest.json").use { stream -> | ||||
|                 if (stream == null) { | ||||
|  | @ -211,14 +210,15 @@ object PluginManager { | |||
|                 loader.loadClass(manifest.pluginClassName) as Class<out Plugin?> | ||||
|             val pluginInstance: Plugin = | ||||
|                 pluginClass.newInstance() as Plugin | ||||
|             if (plugins.containsKey(filePath)) { | ||||
|                 println("Plugin with name $name already exists") | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             // Sets with the proper version | ||||
|             setPluginData(data.copy(version = version)) | ||||
| 
 | ||||
|             if (plugins.containsKey(filePath)) { | ||||
|                 Log.i(TAG, "Plugin with name $name already exists") | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             pluginInstance.__filename = fileName | ||||
|             if (pluginInstance.needsResources) { | ||||
|                 // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk | ||||
|  | @ -228,21 +228,21 @@ object PluginManager { | |||
|                 addAssetPath.invoke(assets, file.absolutePath) | ||||
|                 pluginInstance.resources = Resources( | ||||
|                     assets, | ||||
|                     context.resources.displayMetrics, | ||||
|                     context.resources.configuration | ||||
|                     activity.resources.displayMetrics, | ||||
|                     activity.resources.configuration | ||||
|                 ) | ||||
|             } | ||||
|             plugins[filePath] = pluginInstance | ||||
|             classLoaders[loader] = pluginInstance | ||||
|             pluginInstance.load(context) | ||||
|             println("Loaded plugin ${data.internalName} successfully") | ||||
|             pluginInstance.load(activity) | ||||
|             Log.i(TAG, "Loaded plugin ${data.internalName} successfully") | ||||
|             true | ||||
|         } catch (e: Throwable) { | ||||
|             failedToLoad[file] = e | ||||
|             e.printStackTrace() | ||||
|             showToast( | ||||
|                 context as Activity, | ||||
|                 context.getString(R.string.plugin_load_fail).format(fileName), | ||||
|                 activity, | ||||
|                 activity.getString(R.string.plugin_load_fail).format(fileName), | ||||
|                 Toast.LENGTH_LONG | ||||
|             ) | ||||
|             false | ||||
|  | @ -250,7 +250,7 @@ object PluginManager { | |||
|     } | ||||
| 
 | ||||
|     suspend fun downloadAndLoadPlugin( | ||||
|         context: Context, | ||||
|         activity: Activity, | ||||
|         pluginUrl: String, | ||||
|         internalName: String, | ||||
|         repositoryUrl: String | ||||
|  | @ -260,21 +260,28 @@ object PluginManager { | |||
|             true | ||||
|         ) + "." + repositoryUrl.hashCode()) // Guaranteed unique | ||||
|         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 | ||||
|         val file = downloadPluginToFile(context, pluginUrl, fileName, folderName) | ||||
|         val file = downloadPluginToFile(activity, pluginUrl, fileName, folderName) | ||||
|         return loadPlugin( | ||||
|             context, | ||||
|             activity, | ||||
|             file ?: return false, | ||||
|             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() | ||||
|             .firstOrNull { it.url == pluginUrl } | ||||
|             ?: return false | ||||
|         deletePluginData(data) | ||||
|         return File(data.filePath).delete() | ||||
|         return try { | ||||
|             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.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 | ||||
|  | @ -24,6 +23,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate | |||
| import com.lagradost.cloudstream3.utils.UIHelper.setImage | ||||
| import kotlinx.android.synthetic.main.main_settings.* | ||||
| import kotlinx.android.synthetic.main.settings_title_top.* | ||||
| import kotlinx.android.synthetic.main.standard_toolbar.* | ||||
| import java.io.File | ||||
| 
 | ||||
| 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 | ||||
|             settings_toolbar?.apply { | ||||
|                 setTitle(title) | ||||
|  | @ -138,7 +150,10 @@ 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), | ||||
|             Pair( | ||||
|                 settings_extensions, | ||||
|                 R.id.action_navigation_settings_to_navigation_settings_extensions | ||||
|             ), | ||||
|         ).forEach { (view, navigationId) -> | ||||
|             view?.apply { | ||||
|                 setOnClickListener { | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ 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.ui.settings.SettingsFragment.Companion.setUpToolbar | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||
|  | @ -37,7 +38,9 @@ class ExtensionsFragment : Fragment() { | |||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         context?.fixPaddingStatusbar(extensions_root) | ||||
|         //context?.fixPaddingStatusbar(extensions_root) | ||||
| 
 | ||||
|         setUpToolbar(R.string.extensions) | ||||
| 
 | ||||
|         repo_recycler_view?.adapter = RepoAdapter(emptyArray(), { | ||||
|             findNavController().navigate( | ||||
|  | @ -94,7 +97,5 @@ class ExtensionsFragment : Fragment() { | |||
| 
 | ||||
| 
 | ||||
|         extensionViewModel.loadRepositories() | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -5,8 +5,6 @@ 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, | ||||
|  | @ -24,8 +22,4 @@ class ExtensionsViewModel : ViewModel() { | |||
|         val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray() | ||||
|         _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.ViewGroup | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.recyclerview.widget.DiffUtil | ||||
| 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 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.* | ||||
| 
 | ||||
| 
 | ||||
| data class PluginViewData( | ||||
|     val plugin: Plugin, | ||||
|     val isDownloaded: Boolean, | ||||
| ) | ||||
| 
 | ||||
| class PluginAdapter( | ||||
|     var plugins: List<Pair<String, SitePlugin>>, | ||||
|     val iconClickCallback: PluginAdapter.(repositoryUrl: String, plugin: SitePlugin, isDownloaded: Boolean) -> Unit | ||||
|     val iconClickCallback: (Plugin) -> Unit | ||||
| ) : | ||||
|     RecyclerView.Adapter<RecyclerView.ViewHolder>() { | ||||
|     private val plugins: MutableList<PluginViewData> = mutableListOf() | ||||
| 
 | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { | ||||
|         return PluginViewHolder( | ||||
|             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) { | ||||
|         val (repositoryUrl, plugin) = plugins[position] | ||||
|         when (holder) { | ||||
|             is PluginViewHolder -> { | ||||
|                 holder.bind(repositoryUrl, plugin) | ||||
|                 holder.bind(plugins[position]) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -35,34 +45,61 @@ class PluginAdapter( | |||
|         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() | ||||
| 
 | ||||
|     fun reloadStoredPlugins(): Array<PluginData> { | ||||
|     private fun reloadStoredPlugins(): Array<PluginData> { | ||||
|         return PluginManager.getPluginsOnline().also { storedPlugins = it } | ||||
|     } | ||||
|     }*/ | ||||
| 
 | ||||
|     inner class PluginViewHolder(itemView: View) : | ||||
|         RecyclerView.ViewHolder(itemView) { | ||||
| 
 | ||||
|         fun bind( | ||||
|             repositoryUrl: String, | ||||
|             plugin: SitePlugin, | ||||
|             data: PluginViewData, | ||||
|         ) { | ||||
|             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 | ||||
|             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?.setOnClickListener { | ||||
|                 iconClickCallback.invoke(this@PluginAdapter, repositoryUrl, plugin, isDownloaded) | ||||
|                 iconClickCallback.invoke(data.plugin) | ||||
|             } | ||||
| 
 | ||||
|             itemView.main_text?.text = plugin.name | ||||
|             itemView.sub_text?.text = plugin.description | ||||
|             itemView.main_text?.text = metadata.name | ||||
|             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 com.lagradost.cloudstream3.CommonActivity.showToast | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.mvvm.observe | ||||
| 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.main | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||
|  | @ -27,53 +29,31 @@ class PluginsFragment : Fragment() { | |||
|         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?) { | ||||
|         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) | ||||
|             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 | ||||
|                             } | ||||
|         setUpToolbar(name ?: "Unknown") | ||||
| 
 | ||||
|                             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() | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|         repo_recycler_view?.adapter = | ||||
|             PluginAdapter { | ||||
|                 pluginViewModel.handlePluginAction(activity, url, it) | ||||
|             } | ||||
| 
 | ||||
|         observe(pluginViewModel.plugins) { | ||||
|             (repo_recycler_view?.adapter as? PluginAdapter?)?.updateList(it) | ||||
|         } | ||||
| 
 | ||||
|         pluginViewModel.updatePluginList(url) | ||||
|     } | ||||
| 
 | ||||
|     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"?> | ||||
| 
 | ||||
| <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 | ||||
|         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"> | ||||
| 
 | ||||
|         <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:layout_width="match_parent" | ||||
|             android:layout_height="match_parent" | ||||
|             app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"> | ||||
| 
 | ||||
|         </androidx.recyclerview.widget.RecyclerView> | ||||
| 
 | ||||
|     </LinearLayout> | ||||
|             app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> | ||||
| 
 | ||||
|     <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" /> | ||||
|             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> | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,22 +9,7 @@ | |||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <com.google.android.material.appbar.AppBarLayout | ||||
|             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> | ||||
|     <include layout="@layout/standard_toolbar" /> | ||||
| 
 | ||||
|     <!-- Required ViewGroup for PreferenceFragmentCompat --> | ||||
|     <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…
	
	Add table
		Add a link
		
	
		Reference in a new issue