forked from recloudstream/cloudstream
Revamped backend and added auto updating
This commit is contained in:
parent
8f3176d2cf
commit
89936c2bd6
9 changed files with 216 additions and 111 deletions
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.lagradost.cloudstream3">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.lagradost.cloudstream3">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- I dont remember, probs has to do with downloads -->
|
||||
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
||||
|
@ -12,41 +12,41 @@
|
|||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide-->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!--Used for app update-->
|
||||
<!--<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> not used atm, but code exist that requires it that are not run-->
|
||||
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –>-->
|
||||
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –>-->
|
||||
|
||||
<!-- Fixes android tv fuckery -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
|
||||
<application
|
||||
android:name=".AcraApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:label="@string/app_name"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:appCategory="video"
|
||||
tools:targetApi="o">
|
||||
android:name=".AcraApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="video"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="o">
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="com.lagradost.cloudstream3.utils.CastOptionsProvider" />
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="com.lagradost.cloudstream3.utils.CastOptionsProvider" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.player.DownloadedPlayerActivity"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:exported="true">
|
||||
android:name=".ui.player.DownloadedPlayerActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="true"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:supportsPictureInPicture="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
@ -57,18 +57,18 @@
|
|||
|
||||
<!-- I dont think this label can be translated, but idk -->
|
||||
<intent-filter android:label="@string/play_with_app_name">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true">
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true">
|
||||
<intent-filter android:exported="true">
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
@ -83,35 +83,39 @@
|
|||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="cloudstreamapp" />
|
||||
<data
|
||||
android:host="cs.repo"
|
||||
android:pathPrefix="/"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.VideoDownloadRestartReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
android:name=".receivers.VideoDownloadRestartReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter android:exported="true">
|
||||
<action android:name="restart_service" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name=".services.VideoDownloadService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
android:name=".services.VideoDownloadService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ControllerActivity" />
|
||||
android:name=".ui.ControllerActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:enabled="true"
|
||||
android:grantUriPermissions="true">
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
|
@ -420,6 +420,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
app.initClient(this)
|
||||
|
||||
PluginManager.updateAllOnlinePlugins(applicationContext)
|
||||
PluginManager.loadAllLocalPlugins(applicationContext)
|
||||
PluginManager.loadAllOnlinePlugins(applicationContext)
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ abstract class Plugin {
|
|||
class Manifest {
|
||||
var name: String? = null
|
||||
var pluginClassName: String? = null
|
||||
var pluginVersion: Int? = null
|
||||
}
|
||||
|
||||
var resources: Resources? = null
|
||||
|
|
|
@ -17,6 +17,11 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
|
|||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
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 kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
|
@ -27,13 +32,22 @@ import java.util.*
|
|||
const val PLUGINS_KEY = "PLUGINS_KEY"
|
||||
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
|
||||
|
||||
|
||||
// Data class for internal storage
|
||||
data class PluginData(
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("internalName") val internalName: String,
|
||||
@JsonProperty("url") val url: String?,
|
||||
@JsonProperty("isOnline") val isOnline: Boolean,
|
||||
@JsonProperty("filePath") val filePath: String,
|
||||
@JsonProperty("version") val version: Int,
|
||||
)
|
||||
|
||||
// This is used as a placeholder / not set version
|
||||
const val PLUGIN_VERSION_NOT_SET = Int.MIN_VALUE
|
||||
|
||||
// This always updates
|
||||
const val PLUGIN_VERSION_ALWAYS_UPDATE = -1
|
||||
|
||||
object PluginManager {
|
||||
// Prevent multiple writes at once
|
||||
val lock = Mutex()
|
||||
|
@ -80,22 +94,62 @@ object PluginManager {
|
|||
private val LOCAL_PLUGINS_PATH =
|
||||
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins"
|
||||
|
||||
|
||||
// Maps filepath to plugin
|
||||
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 loadedLocalPlugins = false
|
||||
private var loadedLocalPlugins = false
|
||||
private val gson = Gson()
|
||||
|
||||
private 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))
|
||||
loadPlugin(
|
||||
context,
|
||||
file,
|
||||
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be run before other plugin loading because plugin loading can not be overwritten
|
||||
**/
|
||||
fun updateAllOnlinePlugins(context: Context) {
|
||||
val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
|
||||
|
||||
val onlinePlugins = urls.toList().apmap {
|
||||
getRepoPlugins(it.url)?.toList() ?: emptyList()
|
||||
}.flatten()
|
||||
|
||||
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
|
||||
val outdatedPlugins = getPluginsOnline().map { savedData ->
|
||||
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
|
||||
.mapNotNull { onlineData ->
|
||||
val isOutdated =
|
||||
onlineData.second.apiVersion != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
|
||||
if (isOutdated) savedData to onlineData else null
|
||||
}
|
||||
}.flatten()
|
||||
|
||||
println("Outdated plugins: $outdatedPlugins")
|
||||
|
||||
outdatedPlugins.apmap {
|
||||
downloadAndLoadPlugin(
|
||||
context,
|
||||
it.second.second.url,
|
||||
it.first.internalName,
|
||||
it.second.first
|
||||
)
|
||||
}
|
||||
|
||||
println("Plugin update done!")
|
||||
}
|
||||
|
||||
fun loadAllOnlinePlugins(context: Context) {
|
||||
File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name }
|
||||
?.forEach { file ->
|
||||
|
@ -130,12 +184,12 @@ object PluginManager {
|
|||
* */
|
||||
private fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
|
||||
val fileName = file.nameWithoutExtension
|
||||
setPluginData(data)
|
||||
val filePath = file.absolutePath
|
||||
println("Loading plugin: $data")
|
||||
|
||||
//logger.info("Loading plugin: " + fileName);
|
||||
return try {
|
||||
val loader = PathClassLoader(file.absolutePath, context.classLoader)
|
||||
val loader = PathClassLoader(filePath, context.classLoader)
|
||||
var manifest: Plugin.Manifest
|
||||
loader.getResourceAsStream("manifest.json").use { stream ->
|
||||
if (stream == null) {
|
||||
|
@ -150,15 +204,21 @@ object PluginManager {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
val name: String = manifest.name ?: "NO NAME"
|
||||
val version: Int = manifest.pluginVersion ?: PLUGIN_VERSION_NOT_SET
|
||||
val pluginClass: Class<*> =
|
||||
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
|
||||
val pluginInstance: Plugin =
|
||||
pluginClass.newInstance() as Plugin
|
||||
if (plugins.containsKey(name)) {
|
||||
if (plugins.containsKey(filePath)) {
|
||||
println("Plugin with name $name already exists")
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
// Sets with the proper version
|
||||
setPluginData(data.copy(version = version))
|
||||
|
||||
pluginInstance.__filename = fileName
|
||||
if (pluginInstance.needsResources) {
|
||||
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
|
||||
|
@ -172,9 +232,10 @@ object PluginManager {
|
|||
context.resources.configuration
|
||||
)
|
||||
}
|
||||
plugins[name] = pluginInstance
|
||||
plugins[filePath] = pluginInstance
|
||||
classLoaders[loader] = pluginInstance
|
||||
pluginInstance.load(context)
|
||||
println("Loaded plugin ${data.internalName} successfully")
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
failedToLoad[file] = e
|
||||
|
@ -188,12 +249,24 @@ object PluginManager {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun downloadPlugin(context: Context, pluginUrl: String, name: String): Boolean {
|
||||
val file = downloadPluginToFile(context, pluginUrl, name)
|
||||
suspend fun downloadAndLoadPlugin(
|
||||
context: Context,
|
||||
pluginUrl: String,
|
||||
internalName: String,
|
||||
repositoryUrl: String
|
||||
): Boolean {
|
||||
val folderName = (sanitizeFilename(
|
||||
repositoryUrl,
|
||||
true
|
||||
) + "." + repositoryUrl.hashCode()) // Guaranteed unique
|
||||
val fileName = (sanitizeFilename(internalName, true) + "." + internalName.hashCode())
|
||||
println("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)
|
||||
return loadPlugin(
|
||||
context,
|
||||
file ?: return false,
|
||||
PluginData(name, pluginUrl, true, file.absolutePath)
|
||||
PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,16 +3,13 @@ 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.PROVIDER_STATUS_OK
|
||||
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.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
|
||||
|
@ -36,17 +33,28 @@ data class Repository(
|
|||
* 3: Beta only
|
||||
* */
|
||||
data class SitePlugin(
|
||||
// Url to the .cs3 file
|
||||
@JsonProperty("url") val url: String,
|
||||
// Status to remotely disable the provider
|
||||
@JsonProperty("status") val status: Int,
|
||||
// Integer over 0, any change of this will trigger an auto update
|
||||
@JsonProperty("version") val version: Int,
|
||||
// Unused currently, used to make the api backwards compatible?
|
||||
// Set to 1
|
||||
@JsonProperty("apiVersion") val apiVersion: Int,
|
||||
// Name to be shown in app
|
||||
@JsonProperty("name") val name: String,
|
||||
// Name to be referenced internally. Separate to make name and url changes possible
|
||||
@JsonProperty("internalName") val internalName: String,
|
||||
@JsonProperty("authors") val authors: List<String>,
|
||||
@JsonProperty("description") val description: String?,
|
||||
// Might be used to go directly to the plugin repo in the future
|
||||
@JsonProperty("repositoryUrl") val repositoryUrl: String?,
|
||||
// These types are yet to be mapped and used, ignore for now
|
||||
@JsonProperty("tvTypes") val tvTypes: List<String>?,
|
||||
@JsonProperty("language") val language: String?,
|
||||
@JsonProperty("iconUrl") val iconUrl: String?,
|
||||
// Set to true to get an 18+ symbol next to the plugin
|
||||
@JsonProperty("isAdult") val isAdult: Boolean?,
|
||||
)
|
||||
|
||||
|
@ -69,21 +77,32 @@ object RepositoryManager {
|
|||
return tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun getRepoPlugins(repositoryUrl: String): List<SitePlugin>? {
|
||||
/**
|
||||
* Gets all plugins from repositories and pairs them with the repository url
|
||||
* */
|
||||
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
|
||||
val repo = parseRepository(repositoryUrl) ?: return null
|
||||
return repo.pluginLists.apmap {
|
||||
parsePlugins(it)
|
||||
return repo.pluginLists.apmapIndexed { index, url ->
|
||||
parsePlugins(url).map {
|
||||
repo.pluginLists[index] to it
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
suspend fun downloadPluginToFile(context: Context, pluginUrl: String, name: String): File? {
|
||||
suspend fun downloadPluginToFile(context: Context, pluginUrl: String, fileName: String, folder: String): File? {
|
||||
return suspendSafeApiCall {
|
||||
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
|
||||
val newDir = File(extensionsDir, folder)
|
||||
newDir.mkdirs()
|
||||
|
||||
val newFile = File(newDir, "${fileName}.cs3")
|
||||
// Overwrite if exists
|
||||
if (newFile.exists()) {
|
||||
newFile.delete()
|
||||
}
|
||||
newFile.createNewFile()
|
||||
|
||||
val body = app.get(pluginUrl).okhttpResponse.body
|
||||
|
@ -95,21 +114,20 @@ object RepositoryManager {
|
|||
// 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<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
|
||||
// No duplicates
|
||||
if (currentRepos.any { it.url == repository.url }) return
|
||||
setKey(REPOSITORIES_KEY, currentRepos + repository)
|
||||
}
|
||||
repoLock.withLock {
|
||||
val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
|
||||
// No duplicates
|
||||
setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url })
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeRepository(repository: RepositoryData) {
|
||||
repoLock.withLock {
|
||||
val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
|
||||
// No duplicates
|
||||
val newRepos = currentRepos.filter { it.url != repository.url }
|
||||
setKey(REPOSITORIES_KEY, newRepos)
|
||||
}
|
||||
repoLock.withLock {
|
||||
val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
|
||||
// No duplicates
|
||||
val newRepos = currentRepos.filter { it.url != repository.url }
|
||||
setKey(REPOSITORIES_KEY, newRepos)
|
||||
}
|
||||
}
|
||||
|
||||
private fun write(stream: InputStream, output: OutputStream) {
|
||||
|
|
|
@ -25,7 +25,7 @@ class ExtensionsViewModel : ViewModel() {
|
|||
_repositories.postValue(urls)
|
||||
}
|
||||
|
||||
suspend fun getPlugins(repositoryUrl: String): List<SitePlugin> {
|
||||
suspend fun getPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>> {
|
||||
return RepositoryManager.getRepoPlugins(repositoryUrl) ?: emptyList()
|
||||
}
|
||||
}
|
|
@ -12,8 +12,8 @@ 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
|
||||
var plugins: List<Pair<String, SitePlugin>>,
|
||||
val iconClickCallback: PluginAdapter.(repositoryUrl: String, plugin: SitePlugin, isDownloaded: Boolean) -> Unit
|
||||
) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
@ -23,9 +23,10 @@ class PluginAdapter(
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val (repositoryUrl, plugin) = plugins[position]
|
||||
when (holder) {
|
||||
is PluginViewHolder -> {
|
||||
holder.bind(plugins[position])
|
||||
holder.bind(repositoryUrl, plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +45,8 @@ class PluginAdapter(
|
|||
RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
fun bind(
|
||||
plugin: SitePlugin
|
||||
repositoryUrl: String,
|
||||
plugin: SitePlugin,
|
||||
) {
|
||||
val isDownloaded = storedPlugins.any { it.url == plugin.url }
|
||||
|
||||
|
@ -56,7 +58,7 @@ class PluginAdapter(
|
|||
itemView.action_button?.setImageResource(drawableInt)
|
||||
|
||||
itemView.action_button?.setOnClickListener {
|
||||
iconClickCallback.invoke(this@PluginAdapter, plugin, isDownloaded)
|
||||
iconClickCallback.invoke(this@PluginAdapter, repositoryUrl, plugin, isDownloaded)
|
||||
}
|
||||
|
||||
itemView.main_text?.text = plugin.name
|
||||
|
|
|
@ -10,7 +10,6 @@ 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
|
||||
|
@ -44,29 +43,35 @@ class PluginsFragment : Fragment() {
|
|||
ioSafe {
|
||||
val plugins = extensionViewModel.getPlugins(url)
|
||||
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
|
||||
}
|
||||
repo_recycler_view?.adapter =
|
||||
PluginAdapter(plugins) { repositoryUrl, plugin, isDownloaded ->
|
||||
ioSafe {
|
||||
val (success, message) = if (isDownloaded) {
|
||||
PluginManager.deletePlugin(
|
||||
view.context,
|
||||
plugin.url,
|
||||
plugin.name
|
||||
) to R.string.plugin_deleted
|
||||
} else {
|
||||
PluginManager.downloadAndLoadPlugin(
|
||||
view.context,
|
||||
plugin.url,
|
||||
plugin.name,
|
||||
repositoryUrl
|
||||
) to R.string.plugin_loaded
|
||||
}
|
||||
|
||||
println("Success: $success")
|
||||
if (success) {
|
||||
main {
|
||||
showToast(activity, message, Toast.LENGTH_SHORT)
|
||||
this@PluginAdapter.reloadStoredPlugins()
|
||||
// Dirty and needs a fix
|
||||
repo_recycler_view?.adapter?.notifyDataSetChanged()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -426,11 +426,12 @@ object VideoDownloadManager {
|
|||
}
|
||||
|
||||
private const val reservedChars = "|\\?*<\":>+[]/\'"
|
||||
fun sanitizeFilename(name: String): String {
|
||||
fun sanitizeFilename(name: String, removeSpaces: Boolean= false): String {
|
||||
var tempName = name
|
||||
for (c in reservedChars) {
|
||||
tempName = tempName.replace(c, ' ')
|
||||
}
|
||||
if (removeSpaces) tempName = tempName.replace(" ", "")
|
||||
return tempName.replace(" ", " ").trim(' ')
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue