Added basic extension management

This commit is contained in:
Blatzar 2022-08-07 01:43:39 +02:00
parent e10cb7b0a3
commit 8c0e07decb
15 changed files with 804 additions and 136 deletions

View File

@ -39,7 +39,6 @@ import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
@ -52,16 +51,12 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
@ -76,20 +71,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.schabi.newpipe.extractor.NewPipe
import java.io.File
import kotlin.concurrent.thread
import kotlin.reflect.KClass
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.RepositoryParser
const val VLC_PACKAGE = "org.videolan.vlc"
@ -402,7 +391,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this)
PluginManager.loadAllPlugins(applicationContext)
PluginManager.loadAllLocalPlugins(applicationContext)
PluginManager.loadAllOnlinePlugins(applicationContext)
// ioSafe {
// val plugins =
// RepositoryParser.getRepoPlugins("https://raw.githubusercontent.com/recloudstream/TestPlugin/master/repo.json")

View File

@ -3,26 +3,113 @@ package com.lagradost.cloudstream3.plugins
import android.content.Context
import dalvik.system.PathClassLoader
import com.google.gson.Gson
import com.lagradost.cloudstream3.plugins.PluginManager
import android.content.res.AssetManager
import android.content.res.Resources
import android.os.Environment
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.io.InputStreamReader
import java.util.*
// Different keys for local and not since local can be removed at any time without app knowing, hence the local are getting rebuilt on every app start
const val PLUGINS_KEY = "PLUGINS_KEY"
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
data class PluginData(
@JsonProperty("name") val name: String,
@JsonProperty("url") val url: String?,
@JsonProperty("isOnline") val isOnline: Boolean,
@JsonProperty("filePath") val filePath: String,
)
object PluginManager {
private val PLUGINS_PATH =
// Prevent multiple writes at once
val lock = Mutex()
/**
* Store data about the plugin for fetching later
* */
private fun setPluginData(data: PluginData) {
ioSafe {
lock.withLock {
if (data.isOnline) {
val plugins = getPluginsOnline()
setKey(PLUGINS_KEY, plugins + data)
} else {
val plugins = getPluginsLocal()
setKey(PLUGINS_KEY_LOCAL, plugins + data)
}
}
}
}
private fun deletePluginData(data: PluginData) {
ioSafe {
lock.withLock {
if (data.isOnline) {
val plugins = getPluginsOnline().filter { it.url != data.url }
setKey(PLUGINS_KEY, plugins)
} else {
val plugins = getPluginsLocal().filter { it.filePath != data.filePath }
setKey(PLUGINS_KEY_LOCAL, plugins + data)
}
}
}
}
fun getPluginsOnline(): Array<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"
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 loadedPlugins = false
var loadedLocalPlugins = false
private val gson = Gson()
fun loadAllPlugins(context: Context) {
val dir = File(PLUGINS_PATH)
fun maybeLoadPlugin(context: Context, file: File) {
val name = file.name
if (file.extension == "zip" || file.extension == "cs3") {
loadPlugin(context, file, PluginData(name, null, false, file.absolutePath))
} else if (name != "oat") { // Some roms create this
if (file.isDirectory) {
// Utils.showToast(String.format("Found directory %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true);
} else if (name == "classes.dex" || name.endsWith(".json")) {
// Utils.showToast(String.format("Found extracted plugin file %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true);
}
// rmrf(f);
}
}
fun loadAllOnlinePlugins(context: Context) {
File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name }
?.forEach { file ->
maybeLoadPlugin(context, file)
}
}
fun loadAllLocalPlugins(context: Context) {
val dir = File(LOCAL_PLUGINS_PATH)
removeKey(PLUGINS_KEY_LOCAL)
if (!dir.exists()) {
val res = dir.mkdirs()
if (!res) {
@ -30,39 +117,36 @@ object PluginManager {
return
}
}
val sortedPlugins = dir.listFiles()
// Always sort plugins alphabetically for reproducible results
sortedPlugins?.sortedBy { it.name }?.forEach { file ->
val name = file.name
if (file.extension == "zip" || file.extension == "cs3") {
loadPlugin(context, file)
} else if (name != "oat") { // Some roms create this
if (file.isDirectory) {
// Utils.showToast(String.format("Found directory %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true);
} else if (name == "classes.dex" || name.endsWith(".json")) {
// Utils.showToast(String.format("Found extracted plugin file %s in your plugins folder. DO NOT EXTRACT PLUGIN ZIPS!", name), true);
}
// rmrf(f);
}
maybeLoadPlugin(context, file)
}
loadedPlugins = true
loadedLocalPlugins = true
//if (!PluginManager.failedToLoad.isEmpty())
//Utils.showToast("Some plugins failed to load.");
}
fun loadPlugin(context: Context, file: File) {
/**
* @return True if successful, false if not
* */
private fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
val fileName = file.nameWithoutExtension
setPluginData(data)
println("Loading plugin: $data")
//logger.info("Loading plugin: " + fileName);
try {
return try {
val loader = PathClassLoader(file.absolutePath, context.classLoader)
var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream ->
if (stream == null) {
failedToLoad[file] = "No manifest found"
//logger.error("Failed to load plugin " + fileName + ": No manifest found", null);
return
return false
}
InputStreamReader(stream).use { reader ->
manifest = gson.fromJson(
@ -76,10 +160,10 @@ object PluginManager {
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
val pluginInstance: Plugin =
pluginClass.newInstance() as Plugin
if (plugins.containsKey(name)) {
// if (plugins.containsKey(name)) {
//logger.error("Plugin with name " + name + " already exists", null);
return
}
// return false
// }
pluginInstance.__filename = fileName
if (pluginInstance.needsResources) {
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
@ -96,10 +180,29 @@ object PluginManager {
plugins[name] = pluginInstance
classLoaders[loader] = pluginInstance
pluginInstance.load(context)
true
} catch (e: Throwable) {
failedToLoad[file] = e
e.printStackTrace()
//logger.error("Failed to load plugin " + fileName + ":\n", e);
false
}
}
suspend fun downloadPlugin(context: Context, pluginUrl: String, name: String): Boolean {
val file = downloadPluginToFile(context, pluginUrl, name)
return loadPlugin(
context,
file ?: return false,
PluginData(name, pluginUrl, true, file.absolutePath)
)
}
fun deletePlugin(context: Context, pluginUrl: String, name: String): Boolean {
val data = getPluginsOnline()
.firstOrNull { it.url == pluginUrl }
?: return false
deletePluginData(data)
return File(data.filePath).delete()
}
}

View File

@ -2,10 +2,18 @@ package com.lagradost.cloudstream3.plugins
import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.BufferedInputStream
import java.io.File
import java.io.InputStream
@ -33,7 +41,9 @@ data class SitePlugin(
)
object RepositoryParser {
object RepositoryManager {
const val ONLINE_PLUGINS_FOLDER = "Extensions"
private suspend fun parseRepository(url: String): Repository? {
return suspendSafeApiCall {
// Take manifestVersion and such into account later
@ -56,19 +66,29 @@ object RepositoryParser {
}.filterNotNull().flatten()
}
private suspend fun downloadSiteTemp(context: Context, pluginUrl: String, name: String): File? {
suspend fun downloadPluginToFile(context: Context, pluginUrl: String, name: String): File? {
return suspendSafeApiCall {
val dir = context.cacheDir
val file = File.createTempFile(name, ".cs3", dir)
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
if (!extensionsDir.exists())
extensionsDir.mkdirs()
val newFile = File(extensionsDir, "$name.${pluginUrl.hashCode()}.cs3")
if (newFile.exists()) return@suspendSafeApiCall newFile
newFile.createNewFile()
val body = app.get(pluginUrl).okhttpResponse.body
write(body.byteStream(), file.outputStream())
file
write(body.byteStream(), newFile.outputStream())
newFile
}
}
suspend fun loadSiteTemp(context: Context, pluginUrl: String, name: String) {
val file = downloadSiteTemp(context, pluginUrl, name)
PluginManager.loadPlugin(context, file ?: return)
// Don't want to read before we write in another thread
private val repoLock = Mutex()
suspend fun addRepository(repository: RepositoryData) {
repoLock.withLock {
val currentRepos = getKey<List<RepositoryData>>(REPOSITORIES_KEY) ?: emptyList()
setKey(REPOSITORIES_KEY, currentRepos + repository)
}
}
private fun write(stream: InputStream, output: OutputStream) {

View File

@ -11,6 +11,7 @@ import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
@ -137,6 +138,7 @@ class SettingsFragment : Fragment() {
Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui),
Pair(settings_lang, R.id.action_navigation_settings_to_navigation_settings_lang),
Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates),
Pair(settings_extensions, R.id.action_navigation_settings_to_navigation_settings_extensions),
).forEach { (view, navigationId) ->
view?.apply {
setOnClickListener {

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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
}
}
}

View 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>

View 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>

View File

@ -1,111 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:background="?attr/primaryBlackBackground"
android:layout_height="match_parent">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryBlackBackground">
<ScrollView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:orientation="vertical"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:visibility="gone"
tools:visibility="visible"
android:id="@+id/settings_profile"
android:padding="20dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:id="@+id/settings_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp"
android:visibility="gone"
tools:visibility="visible">
<androidx.cardview.widget.CardView
app:cardCornerRadius="25dp"
android:layout_width="50dp"
android:layout_height="50dp">
android:layout_width="50dp"
android:layout_height="50dp"
app:cardCornerRadius="25dp">
<ImageView
android:id="@+id/settings_profile_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
android:id="@+id/settings_profile_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
</androidx.cardview.widget.CardView>
<TextView
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:id="@+id/settings_profile_text"
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>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View 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>

View File

@ -128,6 +128,41 @@
app:popEnterAnim="@anim/enter_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
android:id="@+id/navigation_settings_lang"
android:name="com.lagradost.cloudstream3.ui.settings.SettingsLang"
@ -306,6 +341,13 @@
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_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

View File

@ -569,4 +569,10 @@
<string name="crash_reporting_title">Crash reporting</string>
<string name="preferred_media_subtext">What do you want to see</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>