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