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.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") | ||||
|  |  | |||
|  | @ -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() | ||||
|     } | ||||
| } | ||||
|  | @ -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) { | ||||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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"?> | ||||
| <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> | ||||
							
								
								
									
										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: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 | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue