forked from recloudstream/cloudstream
		
	Merge remote-tracking branch 'origin/master'
This commit is contained in:
		
						commit
						e5515b575c
					
				
					 22 changed files with 1010 additions and 437 deletions
				
			
		|  | @ -19,6 +19,7 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi | |||
| import com.lagradost.cloudstream3.ui.player.SubtitleData | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.toJson | ||||
| import com.lagradost.cloudstream3.utils.ExtractorLink | ||||
| import okhttp3.Headers | ||||
| import okhttp3.Interceptor | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.* | ||||
|  | @ -107,6 +108,7 @@ object APIHolder { | |||
|             MonoschinosProvider(), | ||||
|             KawaiifuProvider(), // disabled due to cloudflare | ||||
|             //MultiAnimeProvider(), | ||||
| 	        NginxProvider(), | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  | @ -307,6 +309,7 @@ const val PROVIDER_STATUS_DOWN = 0 | |||
| data class ProvidersInfoJson( | ||||
|     @JsonProperty("name") var name: String, | ||||
|     @JsonProperty("url") var url: String, | ||||
|     @JsonProperty("credentials") var credentials: String? = null, | ||||
|     @JsonProperty("status") var status: Int, | ||||
| ) | ||||
| 
 | ||||
|  | @ -319,6 +322,7 @@ abstract class MainAPI { | |||
|     fun overrideWithNewData(data: ProvidersInfoJson) { | ||||
|         this.name = data.name | ||||
|         this.mainUrl = data.url | ||||
| 	    this.storedCredentials = data.credentials | ||||
|     } | ||||
| 
 | ||||
|     init { | ||||
|  | @ -329,6 +333,7 @@ abstract class MainAPI { | |||
| 
 | ||||
|     open var name = "NONE" | ||||
|     open var mainUrl = "NONE" | ||||
|     open var storedCredentials: String? = null | ||||
| 
 | ||||
|     //open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id | ||||
| 
 | ||||
|  | @ -591,17 +596,22 @@ fun getQualityFromString(string: String?): SearchQuality? { | |||
|         "cam" -> SearchQuality.Cam | ||||
|         "camrip" -> SearchQuality.CamRip | ||||
|         "hdcam" -> SearchQuality.HdCam | ||||
|         "hdtc" -> SearchQuality.HdCam | ||||
|         "hdts" -> SearchQuality.HdCam | ||||
|         "highquality" -> SearchQuality.HQ | ||||
|         "hq" -> SearchQuality.HQ | ||||
|         "highdefinition" -> SearchQuality.HD | ||||
|         "hdrip" -> SearchQuality.HD | ||||
|         "hd" -> SearchQuality.HD | ||||
|         "hdtv" -> SearchQuality.HD | ||||
|         "rip" -> SearchQuality.CamRip | ||||
|         "telecine" -> SearchQuality.Telecine | ||||
|         "tc" -> SearchQuality.Telecine | ||||
|         "telesync" -> SearchQuality.Telesync | ||||
|         "ts" -> SearchQuality.Telesync | ||||
|         "dvd" -> SearchQuality.DVD | ||||
|         "dvdrip" -> SearchQuality.DVD | ||||
|         "dvdscr" -> SearchQuality.DVD | ||||
|         "blueray" -> SearchQuality.BlueRay | ||||
|         "bluray" -> SearchQuality.BlueRay | ||||
|         "br" -> SearchQuality.BlueRay | ||||
|  | @ -613,6 +623,7 @@ fun getQualityFromString(string: String?): SearchQuality? { | |||
|         "wp" -> SearchQuality.WorkPrint | ||||
|         "workprint" -> SearchQuality.WorkPrint | ||||
|         "webrip" -> SearchQuality.WebRip | ||||
|         "webdl" -> SearchQuality.WebRip | ||||
|         "web" -> SearchQuality.WebRip | ||||
|         "hdr" -> SearchQuality.HDR | ||||
|         "sdr" -> SearchQuality.SDR | ||||
|  |  | |||
|  | @ -64,12 +64,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.movieproviders.NginxProvider | ||||
| 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 java.io.File | ||||
| import kotlin.collections.HashMap | ||||
| import kotlin.concurrent.thread | ||||
| 
 | ||||
| 
 | ||||
|  | @ -360,6 +362,57 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|             e.printStackTrace() | ||||
|             false | ||||
|         } | ||||
|     fun addNginxToJson(data: java.util.HashMap<String, ProvidersInfoJson>): java.util.HashMap<String, ProvidersInfoJson>? { | ||||
|         try { | ||||
|             val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) | ||||
|             val nginxUrl = | ||||
|                 settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key").toString() | ||||
|             val nginxCredentials = | ||||
|                 settingsManager.getString(getString(R.string.nginx_credentials), "nginx_credentials") | ||||
|                     .toString() | ||||
|             val StoredNginxProvider = NginxProvider() | ||||
|             if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value, or empty: | ||||
|                 data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson( | ||||
|                     url = nginxUrl, | ||||
|                     name = StoredNginxProvider.name, | ||||
|                     status = PROVIDER_STATUS_DOWN,  // the provider will not be display | ||||
|                     credentials = nginxCredentials | ||||
|                 ) | ||||
|             } else {  // valid url | ||||
|                 data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson( | ||||
|                     url = nginxUrl, | ||||
|                     name = StoredNginxProvider.name, | ||||
|                     status = PROVIDER_STATUS_OK, | ||||
|                     credentials = nginxCredentials | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             return data | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return data | ||||
|         } | ||||
|     } | ||||
|     fun createNginxJson() : ProvidersInfoJson? { //java.util.HashMap<String, ProvidersInfoJson> | ||||
|         return try { | ||||
|             val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) | ||||
|             val nginxUrl = settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key").toString() | ||||
|             val nginxCredentials = settingsManager.getString(getString(R.string.nginx_credentials), "nginx_credentials").toString() | ||||
|             if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value or empty: | ||||
|                 null // don't overwrite anything | ||||
|             } else { | ||||
|                 ProvidersInfoJson( | ||||
|                     url = nginxUrl, | ||||
|                     name = NginxProvider().name, | ||||
|                     status = PROVIDER_STATUS_OK, | ||||
|                     credentials = nginxCredentials | ||||
|                 ) | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         // this pulls the latest data so ppl don't have to update to simply change provider url | ||||
|         if (downloadFromGithub) { | ||||
|  | @ -379,8 +432,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|                                             tryParseJson<HashMap<String, ProvidersInfoJson>>(txt) | ||||
|                                         setKey(PROVIDER_STATUS_KEY, txt) | ||||
|                                         MainAPI.overrideData = newCache // update all new providers | ||||
|                                         for (api in apis) { // update current providers | ||||
|                                             newCache?.get(api.javaClass.simpleName)?.let { data -> | ||||
|                                          | ||||
|                                         val newUpdatedCache = newCache?.let { addNginxToJson(it) ?: it } | ||||
| 
 | ||||
| 					                    for (api in apis) { // update current providers | ||||
|                                             newUpdatedCache?.get(api.javaClass.simpleName)?.let { data -> | ||||
|                                                 api.overrideWithNewData(data) | ||||
|                                             } | ||||
|                                         } | ||||
|  | @ -397,12 +453,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|                                 newCache | ||||
|                             }?.let { providersJsonMap -> | ||||
|                                 MainAPI.overrideData = providersJsonMap | ||||
|                                 val providersJsonMapUpdated = addNginxToJson(providersJsonMap)?: providersJsonMap // if return null, use unchanged one | ||||
|                                 val acceptableProviders = | ||||
|                                     providersJsonMap.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } | ||||
|                                     providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } | ||||
|                                         .map { it.key }.toSet() | ||||
| 
 | ||||
|                                 val restrictedApis = | ||||
|                                     if (hasBenene) providersJsonMap.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } | ||||
|                                     if (hasBenene) providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } | ||||
|                                         .map { it.key }.toSet() else emptySet() | ||||
| 
 | ||||
|                                 apis = allProviders.filter { api -> | ||||
|  | @ -425,6 +482,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|             } | ||||
|         } else { | ||||
|             apis = allProviders | ||||
|             try { | ||||
|                 val nginxProviderName = NginxProvider().name | ||||
|                 val nginxProviderIndex = apis.indexOf(APIHolder.getApiFromName(nginxProviderName)) | ||||
|                 val createdJsonProvider = createNginxJson() | ||||
|                 if (createdJsonProvider != null) { | ||||
|                     apis[nginxProviderIndex].overrideWithNewData(createdJsonProvider) // people will have access to it if they disable metadata check (they are not filtered) | ||||
|                 } | ||||
|             } catch (e: Exception) { | ||||
|                 logError(e) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         loadThemes(this) | ||||
|  | @ -619,4 +687,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|         val output = src?.doMath() | ||||
|         println("MASTER OUTPUT = $output")*/ | ||||
|     } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -30,6 +30,14 @@ class VizcloudLive : WcoStream() { | |||
|     override var mainUrl = "https://vizcloud.live" | ||||
| } | ||||
| 
 | ||||
| class VizcloudInfo : WcoStream() { | ||||
|     override var mainUrl = "https://vizcloud.info" | ||||
| } | ||||
| 
 | ||||
| class MwvnVizcloudInfo : WcoStream() { | ||||
|     override var mainUrl = "https://mwvn.vizcloud.info" | ||||
| } | ||||
| 
 | ||||
| open class WcoStream : ExtractorApi() { | ||||
|     override var name = "VidStream" //Cause works for animekisa and wco | ||||
|     override var mainUrl = "https://vidstream.pro" | ||||
|  | @ -103,8 +111,9 @@ open class WcoStream : ExtractorApi() { | |||
|                     } | ||||
|                 } | ||||
|                 if (mainUrl == "https://vidstream.pro" || mainUrl == "https://vidstreamz.online" || mainUrl == "https://vizcloud2.online" | ||||
|                     || mainUrl == "https://vizcloud.xyz" || mainUrl == "https://vizcloud.live") { | ||||
|                 if (it.file.contains("m3u8")) { | ||||
|                     || mainUrl == "https://vizcloud.xyz" || mainUrl == "https://vizcloud.live" || mainUrl == "https://vizcloud.info" | ||||
|                      || mainUrl == "https://mwvn.vizcloud.info") { | ||||
|                  if (it.file.contains("m3u8")) { | ||||
|                     hlsHelper.m3u8Generation(M3u8Helper.M3u8Stream(it.file.replace("#.mp4",""), null, | ||||
|                     headers = mapOf("Referer" to url)), true) | ||||
|                         .forEach { stream -> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import org.jsoup.nodes.Element | |||
| 
 | ||||
| class EgyBestProvider : MainAPI() { | ||||
|     override val lang = "ar" | ||||
|     override var mainUrl = "https://egy.best" | ||||
|     override var mainUrl = "https://www.egy.best" | ||||
|     override var name = "EgyBest" | ||||
|     override val usesWebView = false | ||||
|     override val hasMainPage = true | ||||
|  | @ -26,6 +26,7 @@ class EgyBestProvider : MainAPI() { | |||
|         val isMovie = Regex(".*/movie/.*|.*/masrahiya/.*").matches(url) | ||||
|         val tvType = if (isMovie) TvType.Movie else TvType.TvSeries | ||||
|         title = if (year !== null) title else title.split(" (")[0].trim() | ||||
|         val quality = select("span.ribbon span").text().replace("-", "") | ||||
|         // If you need to differentiate use the url. | ||||
|         return MovieSearchResponse( | ||||
|             title, | ||||
|  | @ -35,18 +36,22 @@ class EgyBestProvider : MainAPI() { | |||
|             posterUrl, | ||||
|             year, | ||||
|             null, | ||||
|             quality = getQualityFromString(quality) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun getMainPage(): HomePageResponse { | ||||
|         // url, title | ||||
|         val doc = app.get(mainUrl).document | ||||
|         val pages = doc.select("#mainLoad div.mbox").apmap { | ||||
|         val pages = arrayListOf<HomePageList>() | ||||
|             doc.select("#mainLoad div.mbox").apmap { | ||||
|             val name = it.select(".bdb.pda > strong").text() | ||||
|             val list = it.select(".movie").mapNotNull { element -> | ||||
|                 element.toSearchResponse() | ||||
|             if (it.select(".movie").first().attr("href").contains("season-(.....)|ep-(.....)".toRegex())) return@apmap | ||||
|             val list = arrayListOf<SearchResponse>() | ||||
|             it.select(".movie").map { element -> | ||||
|                 list.add(element.toSearchResponse()!!) | ||||
|             } | ||||
|             HomePageList(name, list) | ||||
|             pages.add(HomePageList(name, list)) | ||||
|         } | ||||
|         return HomePageResponse(pages) | ||||
|     } | ||||
|  | @ -72,7 +77,7 @@ class EgyBestProvider : MainAPI() { | |||
|         val isMovie = Regex(".*/movie/.*|.*/masrahiya/.*").matches(url) | ||||
|         val posterUrl = doc.select("div.movie_img a img")?.attr("src") | ||||
|         val year = doc.select("div.movie_title h1 a")?.text()?.toIntOrNull() | ||||
|         val title = doc.select("div.movie_title h1 span[itemprop=\"name\"]").text() | ||||
|         val title = doc.select("div.movie_title h1 span").text() | ||||
| 
 | ||||
|         val synopsis = doc.select("div.mbox").firstOrNull { | ||||
|             it.text().contains("القصة") | ||||
|  |  | |||
|  | @ -0,0 +1,268 @@ | |||
| package com.lagradost.cloudstream3.movieproviders | ||||
| 
 | ||||
| import com.lagradost.cloudstream3.* | ||||
| import com.lagradost.cloudstream3.LoadResponse.Companion.addRating | ||||
| import com.lagradost.cloudstream3.utils.ExtractorLink | ||||
| import com.lagradost.cloudstream3.TvType | ||||
| import com.lagradost.cloudstream3.app | ||||
| import com.lagradost.cloudstream3.utils.Qualities | ||||
| import java.lang.Exception | ||||
| 
 | ||||
| class NginxProvider : MainAPI() { | ||||
|     override var name = "Nginx" | ||||
|     override val hasQuickSearch = false | ||||
|     override val hasMainPage = true | ||||
|     override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     fun getAuthHeader(storedCredentials: String?): Map<String, String> { | ||||
|         if (storedCredentials == null) { | ||||
|             return mapOf(Pair("Authorization", "Basic "))  // no Authorization headers | ||||
|         } | ||||
|         val basicAuthToken = base64Encode(storedCredentials.toByteArray())  // will this be loaded when not using the provider ??? can increase load | ||||
|         return mapOf(Pair("Authorization", "Basic $basicAuthToken")) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun load(url: String): LoadResponse { | ||||
|         val authHeader = getAuthHeader(storedCredentials)  // call again because it isn't reloaded if in main class and storedCredentials loads after | ||||
|         // url can be tvshow.nfo for series or mediaRootUrl for movies | ||||
| 
 | ||||
|         val mediaRootDocument = app.get(url, authHeader).document | ||||
| 
 | ||||
|         val nfoUrl = url + mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href")  // metadata url file | ||||
| 
 | ||||
|         val metadataDocument = app.get(nfoUrl, authHeader).document  // get the metadata nfo file | ||||
| 
 | ||||
|         val isMovie = !nfoUrl.contains("tvshow.nfo") | ||||
| 
 | ||||
|         val title = metadataDocument.selectFirst("title").text() | ||||
| 
 | ||||
|         val description = metadataDocument.selectFirst("plot").text() | ||||
| 
 | ||||
|         if (isMovie) { | ||||
|             val poster = metadataDocument.selectFirst("thumb").text() | ||||
|             val trailer = metadataDocument.select("trailer")?.mapNotNull { | ||||
|                it?.text()?.replace( | ||||
|                    "plugin://plugin.video.youtube/play/?video_id=", | ||||
|                    "https://www.youtube.com/watch?v=" | ||||
|                ) | ||||
|             } | ||||
|             val partialUrl = mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href").replace(".nfo", ".") | ||||
|             val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull() | ||||
|             val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull() | ||||
|             val tagsList = metadataDocument.select("genre") | ||||
|                 ?.mapNotNull {   // all the tags like action, thriller ... | ||||
|                     it?.text() | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|             val dataList = mediaRootDocument.getElementsByAttributeValueContaining(  // list of all urls of the webpage | ||||
|                 "href", | ||||
|                 partialUrl | ||||
|             ) | ||||
| 
 | ||||
|             val data = url + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") &&  !it.attr("href").contains(".jpg"))} }.attr("href").toString()  // exclude poster and nfo (metadata) file | ||||
| 
 | ||||
| 
 | ||||
|             return MovieLoadResponse( | ||||
|                 title, | ||||
|                 data, | ||||
|                 this.name, | ||||
|                 TvType.Movie, | ||||
|                 data, | ||||
|                 poster, | ||||
|                 date, | ||||
|                 description, | ||||
|                 ratingAverage, | ||||
|                 tagsList, | ||||
|                 null, | ||||
|                 trailer, | ||||
|                 null, | ||||
|                 null, | ||||
|             ) | ||||
|         } else  // a tv serie | ||||
|         { | ||||
| 
 | ||||
|             val list = ArrayList<Pair<Int, String>>() | ||||
|             val mediaRootUrl = url.replace("tvshow.nfo", "") | ||||
|             val posterUrl = mediaRootUrl + "poster.jpg" | ||||
|             val mediaRootDocument = app.get(mediaRootUrl, authHeader).document | ||||
|             val seasons = | ||||
|                 mediaRootDocument.getElementsByAttributeValueContaining("href", "Season%20") | ||||
| 
 | ||||
| 
 | ||||
|             val tagsList = metadataDocument.select("genre") | ||||
|                 ?.mapNotNull {   // all the tags like action, thriller ...; unused variable | ||||
|                     it?.text() | ||||
|                 } | ||||
| 
 | ||||
|             //val actorsList = document.select("actor") | ||||
|             //    ?.mapNotNull {   // all the tags like action, thriller ...; unused variable | ||||
|             //        it?.text() | ||||
|             //    } | ||||
| 
 | ||||
|             seasons.forEach { element -> | ||||
|                 val season = | ||||
|                     element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull() | ||||
|                 val href = mediaRootUrl + element.attr("href") | ||||
|                 if (season != null && season > 0 && href.isNotBlank()) { | ||||
|                     list.add(Pair(season, href)) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (list.isEmpty()) throw ErrorLoadingException("No Seasons Found") | ||||
| 
 | ||||
|             val episodeList = ArrayList<Episode>() | ||||
| 
 | ||||
| 
 | ||||
|             list.apmap { (seasonInt, seasonString) -> | ||||
|                 val seasonDocument = app.get(seasonString, authHeader).document | ||||
|                 val episodes = seasonDocument.getElementsByAttributeValueContaining( | ||||
|                     "href", | ||||
|                     ".nfo" | ||||
|                 ) // get metadata | ||||
|                     episodes.forEach { episode -> | ||||
|                         val nfoDocument = app.get(seasonString + episode.attr("href"), authHeader).document // get episode metadata file | ||||
|                         val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull() | ||||
|                         val poster = | ||||
|                             seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg") | ||||
|                         val name = nfoDocument.selectFirst("title").text() | ||||
|                         // val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull() | ||||
|                         val date = nfoDocument.selectFirst("aired")?.text() | ||||
|                         val plot = nfoDocument.selectFirst("plot")?.text() | ||||
| 
 | ||||
|                         val dataList = seasonDocument.getElementsByAttributeValueContaining( | ||||
|                             "href", | ||||
|                             episode.attr("href").replace(".nfo", "") | ||||
|                         ) | ||||
|                         val data = seasonString + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") &&  !it.attr("href").contains(".jpg"))} }.attr("href").toString()  // exclude poster and nfo (metadata) file | ||||
| 
 | ||||
|                         episodeList.add( | ||||
|                             newEpisode(data) { | ||||
|                                     this.name = name | ||||
|                                     this.season = seasonInt | ||||
|                                     this.episode = epNum | ||||
|                                     this.posterUrl = poster | ||||
|                                     addDate(date) | ||||
|                                     this.description = plot | ||||
|                             } | ||||
|                         ) | ||||
|                     } | ||||
|             } | ||||
|             return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) { | ||||
|                 this.name = title | ||||
|                 this.url = url | ||||
|                 this.posterUrl = posterUrl | ||||
|                 this.episodes = episodeList | ||||
|                 this.plot = description | ||||
|                 this.tags = tagsList | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     override suspend fun loadLinks( | ||||
|         data: String, | ||||
|         isCasting: Boolean, | ||||
|         subtitleCallback: (SubtitleFile) -> Unit, | ||||
|         callback: (ExtractorLink) -> Unit | ||||
|     ): Boolean { | ||||
|         // loadExtractor(data, null) { callback(it.copy(headers=authHeader)) } | ||||
|         val authHeader = getAuthHeader(storedCredentials)  // call again because it isn't reloaded if in main class and storedCredentials loads after | ||||
|         callback.invoke ( | ||||
|             ExtractorLink( | ||||
|                 name, | ||||
|                 name, | ||||
|                 data, | ||||
|                 data,  // referer not needed | ||||
|                 Qualities.Unknown.value, | ||||
|                 false, | ||||
|                 authHeader, | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     override suspend fun getMainPage(): HomePageResponse? { | ||||
|         val authHeader = getAuthHeader(storedCredentials)  // call again because it isn't reloaded if in main class and storedCredentials loads after | ||||
|         if (mainUrl == "NONE"){ | ||||
|             throw ErrorLoadingException("No nginx url specified in the settings: Nginx Settigns > Nginx server url, try again in a few seconds") | ||||
|         } | ||||
|         val document = app.get(mainUrl, authHeader).document | ||||
|         val categories = document.select("a") | ||||
|         val returnList = categories.mapNotNull { | ||||
|             val categoryPath = mainUrl + it.attr("href") ?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/ | ||||
|             val categoryTitle = it.text()  // get the category title like Movies or Series | ||||
|             if (categoryTitle != "../" && categoryTitle != "Music/") {  // exclude parent dir and Music dir | ||||
|                 val categoryDocument = app.get(categoryPath, authHeader).document // queries the page http://192.168.1.10/media/Movies/ | ||||
|                 val contentLinks = categoryDocument.select("a") | ||||
|                 val currentList = contentLinks.mapNotNull { head -> | ||||
|                     if (head.attr("href") != "../") { | ||||
|                         try { | ||||
|                             val mediaRootUrl = | ||||
|                                 categoryPath + head.attr("href")// like http://192.168.1.10/media/Series/Chernobyl/ | ||||
|                             val mediaDocument = app.get(mediaRootUrl, authHeader).document | ||||
|                             val nfoFilename = mediaDocument.getElementsByAttributeValueContaining( | ||||
|                                 "href", | ||||
|                                 ".nfo" | ||||
|                             )[0].attr("href") | ||||
|                             val isMovieType = nfoFilename != "tvshow.nfo" | ||||
|                             val nfoPath = | ||||
|                                 mediaRootUrl + nfoFilename // must exist or will raise errors, only the first one is taken | ||||
|                             val nfoContent = | ||||
|                                 app.get(nfoPath, authHeader).document  // all the metadata | ||||
| 
 | ||||
| 
 | ||||
|                             if (isMovieType) { | ||||
|                                 val movieName = nfoContent.select("title").text() | ||||
| 
 | ||||
|                                 val posterUrl = mediaRootUrl + "poster.jpg" | ||||
| 
 | ||||
|                                 return@mapNotNull MovieSearchResponse( | ||||
|                                     movieName, | ||||
|                                     mediaRootUrl, | ||||
|                                     this.name, | ||||
|                                     TvType.Movie, | ||||
|                                     posterUrl, | ||||
|                                     null, | ||||
|                                 ) | ||||
|                             } else {  // tv serie | ||||
|                                 val serieName = nfoContent.select("title").text() | ||||
| 
 | ||||
|                                 val posterUrl = mediaRootUrl + "poster.jpg" | ||||
| 
 | ||||
|                                 TvSeriesSearchResponse( | ||||
|                                     serieName, | ||||
|                                     nfoPath, | ||||
|                                     this.name, | ||||
|                                     TvType.TvSeries, | ||||
|                                     posterUrl, | ||||
|                                     null, | ||||
|                                     null, | ||||
|                                 ) | ||||
| 
 | ||||
| 
 | ||||
|                             } | ||||
|                         } catch (e: Exception) {  // can cause issues invisible errors | ||||
|                             null | ||||
|                             //logError(e) // not working because it changes the return type of currentList to Any | ||||
|                         } | ||||
| 
 | ||||
| 
 | ||||
|                     } else null | ||||
|                 } | ||||
|                 if (currentList.isNotEmpty() && categoryTitle != "../") {  // exclude upper dir | ||||
|                     HomePageList(categoryTitle, currentList) | ||||
|                 } else null | ||||
|             } else null  // the path is ../ which is parent directory | ||||
|         } | ||||
|         // if (returnList.isEmpty()) return null // maybe doing nothing idk | ||||
|         return HomePageResponse(returnList) | ||||
|     } | ||||
| } | ||||
|  | @ -663,7 +663,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         @JsonProperty("name") val name: String, | ||||
|         @JsonProperty("location") val location: String, | ||||
|         @JsonProperty("joined_at") val joined_at: String, | ||||
|         @JsonProperty("picture") val picture: String, | ||||
|         @JsonProperty("picture") val picture: String?, | ||||
|     ) | ||||
| 
 | ||||
|     data class MalMainPicture( | ||||
|  | @ -694,4 +694,4 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         val id: Int, | ||||
|         val name: String, | ||||
|     ) | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -545,10 +545,14 @@ class GeneratorPlayer : FullScreenPlayer() { | |||
|                 tvType = meta.tvType | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         player_episode_filler_holder?.isVisible = isFiller ?: false | ||||
| 
 | ||||
|         player_video_title?.text = if (headerName != null) { | ||||
|         //Get limit of characters on Video Title | ||||
|         var limitTitle = 0 | ||||
|         context?.let { | ||||
|             val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) | ||||
|             limitTitle = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0) | ||||
|         } | ||||
|         //Generate video title | ||||
|         var playerVideoTitle = if (headerName != null) { | ||||
|             (headerName + | ||||
|                     if (tvType.isEpisodeBased() && episode != null) | ||||
|                         if (season == null) | ||||
|  | @ -559,6 +563,15 @@ class GeneratorPlayer : FullScreenPlayer() { | |||
|         } else { | ||||
|             "" | ||||
|         } | ||||
|         //Truncate video title if it exceeds limit | ||||
|         val differenceInLength = playerVideoTitle.length - limitTitle | ||||
|         val margin = 3 //If the difference is smaller than or equal to this value, ignore it | ||||
|         if (limitTitle > 0 && differenceInLength > margin) { | ||||
|             playerVideoTitle = playerVideoTitle.substring(0, limitTitle-1) + "..." | ||||
|         } | ||||
| 
 | ||||
|         player_episode_filler_holder?.isVisible = isFiller ?: false | ||||
|         player_video_title?.text = playerVideoTitle | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("SetTextI18n") | ||||
|  |  | |||
|  | @ -47,6 +47,7 @@ import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate | |||
| import com.lagradost.cloudstream3.utils.Qualities | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog | ||||
| import com.lagradost.cloudstream3.utils.SubtitleHelper | ||||
| import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso | ||||
|  | @ -485,6 +486,32 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|             return@setOnPreferenceClickListener true | ||||
|         } | ||||
| 
 | ||||
| 	getPref(R.string.nginx_url_key)?.setOnPreferenceClickListener { | ||||
| 
 | ||||
|             activity?.showNginxTextInputDialog( | ||||
|                 settingsManager.getString(getString(R.string.nginx_url_pref), "Nginx server url").toString(), | ||||
|                 settingsManager.getString(getString(R.string.nginx_url_key), "").toString(),  // key: the actual you use rn | ||||
|                 android.text.InputType.TYPE_TEXT_VARIATION_URI,  // uri | ||||
|                 {}) { | ||||
|                 settingsManager.edit() | ||||
|                     .putString(getString(R.string.nginx_url_key), it).apply()  // change the stored url in nginx_url_key to it | ||||
|             } | ||||
|             return@setOnPreferenceClickListener true | ||||
|         } | ||||
| 
 | ||||
|         getPref(R.string.nginx_credentials)?.setOnPreferenceClickListener { | ||||
| 
 | ||||
|             activity?.showNginxTextInputDialog( | ||||
|                 settingsManager.getString(getString(R.string.nginx_credentials_title), "Nginx Credentials").toString(), | ||||
|                 settingsManager.getString(getString(R.string.nginx_credentials), "").toString(),  // key: the actual you use rn | ||||
|                 android.text.InputType.TYPE_TEXT_VARIATION_URI, | ||||
|                 {}) { | ||||
|                 settingsManager.edit() | ||||
|                     .putString(getString(R.string.nginx_credentials), it).apply()  // change the stored url in nginx_url_key to it | ||||
|             } | ||||
|             return@setOnPreferenceClickListener true | ||||
|         } | ||||
| 
 | ||||
|         getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener { | ||||
|             val prefNames = resources.getStringArray(R.array.media_type_pref) | ||||
|             val prefValues = resources.getIntArray(R.array.media_type_pref_values) | ||||
|  | @ -678,6 +705,24 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|             return@setOnPreferenceClickListener true | ||||
|         } | ||||
| 
 | ||||
|         getPref(R.string.prefer_limit_title_key)?.setOnPreferenceClickListener { | ||||
|             val prefNames = resources.getStringArray(R.array.limit_title_pref_names) | ||||
|             val prefValues = resources.getIntArray(R.array.limit_title_pref_values) | ||||
|             val current = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0) | ||||
| 
 | ||||
|             activity?.showBottomDialog( | ||||
|                 prefNames.toList(), | ||||
|                 prefValues.indexOf(current), | ||||
|                 getString(R.string.limit_title), | ||||
|                 true, | ||||
|                 {}) { | ||||
|                 settingsManager.edit() | ||||
|                     .putInt(getString(R.string.prefer_limit_title_key), prefValues[it]) | ||||
|                     .apply() | ||||
|             } | ||||
|             return@setOnPreferenceClickListener true | ||||
|         } | ||||
| 
 | ||||
|         getPref(R.string.dns_key)?.setOnPreferenceClickListener { | ||||
|             val prefNames = resources.getStringArray(R.array.dns_pref) | ||||
|             val prefValues = resources.getIntArray(R.array.dns_pref_values) | ||||
|  |  | |||
|  | @ -100,6 +100,8 @@ val extractorApis: Array<ExtractorApi> = arrayOf( | |||
|     VizcloudOnline(), | ||||
|     VizcloudXyz(), | ||||
|     VizcloudLive(), | ||||
|     VizcloudInfo(), | ||||
|     MwvnVizcloudInfo(), | ||||
|     Mp4Upload(), | ||||
|     StreamTape(), | ||||
|     MixDrop(), | ||||
|  |  | |||
|  | @ -144,6 +144,46 @@ object SingleSelectionHelper { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     private fun Activity.showInputDialog( | ||||
|         dialog: Dialog, | ||||
|         value: String, | ||||
|         name: String, | ||||
|         textInputType: Int?, | ||||
|         callback: (String) -> Unit, | ||||
|         dismissCallback: () -> Unit | ||||
|     ) { | ||||
|         val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!! | ||||
|         val textView = dialog.findViewById<TextView>(R.id.text1)!! | ||||
|         val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!! | ||||
|         val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!! | ||||
|         val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!! | ||||
| 
 | ||||
|         applyHolder.isVisible = true | ||||
|         textView.text = name | ||||
| 
 | ||||
|         if (textInputType != null) { | ||||
|             inputView.inputType = textInputType // 16 for website url input type | ||||
|         } | ||||
|         inputView.setText(value, TextView.BufferType.EDITABLE) | ||||
| 
 | ||||
| 
 | ||||
|         applyButton.setOnClickListener { | ||||
|             callback.invoke(inputView.text.toString())  // try to save the setting, using callback | ||||
|             dialog.dismissSafe(this) | ||||
|         } | ||||
| 
 | ||||
|         cancelButton.setOnClickListener {  // just dismiss | ||||
|             dialog.dismissSafe(this) | ||||
|         } | ||||
| 
 | ||||
|         dialog.setOnDismissListener { | ||||
|             dismissCallback.invoke() | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     fun Activity.showMultiDialog( | ||||
|         items: List<String>, | ||||
|         selectedIndex: List<Int>, | ||||
|  | @ -192,7 +232,7 @@ object SingleSelectionHelper { | |||
|         selectedIndex: Int, | ||||
|         name: String, | ||||
|         showApply: Boolean, | ||||
|         dismissCallback: () -> Unit, | ||||
|          dismissCallback: () -> Unit, | ||||
|         callback: (Int) -> Unit, | ||||
|     ) { | ||||
|         val builder = | ||||
|  | @ -211,4 +251,25 @@ object SingleSelectionHelper { | |||
|             dismissCallback | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|         fun Activity.showNginxTextInputDialog( | ||||
|             name: String, | ||||
|             value: String, | ||||
|             textInputType: Int?, | ||||
|             dismissCallback: () -> Unit, | ||||
|             callback: (String) -> Unit, | ||||
|     ) { | ||||
|         val builder = BottomSheetDialog(this)  // probably the stuff at the bottom | ||||
|         builder.setContentView(R.layout.bottom_input_dialog)  // input layout | ||||
| 
 | ||||
|         builder.show() | ||||
|         showInputDialog( | ||||
|             builder, | ||||
|             value, | ||||
|             name, | ||||
|             textInputType,  // type is a uri | ||||
|             callback, | ||||
|             dismissCallback | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_baseline_text_format_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/drawable/ic_baseline_text_format_24.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportWidth="24" | ||||
|     android:viewportHeight="24" | ||||
|     android:tint="?attr/white"> | ||||
|   <path | ||||
|       android:fillColor="@android:color/white" | ||||
|       android:pathData="M5,17v2h14v-2L5,17zM9.5,12.8h5l0.9,2.2h2.1L12.75,4h-1.5L6.5,15h2.1l0.9,-2.2zM12,5.98L13.87,11h-3.74L12,5.98z"/> | ||||
| </vector> | ||||
							
								
								
									
										62
									
								
								app/src/main/res/layout/bottom_input_dialog.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								app/src/main/res/layout/bottom_input_dialog.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | |||
| <?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:nextFocusDown="@id/nginx_text_input" | ||||
|         android:orientation="vertical" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <TextView | ||||
|         android:id="@+id/text1" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_rowWeight="1" | ||||
|         android:layout_marginTop="20dp" | ||||
|         android:layout_marginBottom="10dp" | ||||
|         android:paddingStart="?android:attr/listPreferredItemPaddingStart" | ||||
|         android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" | ||||
|         android:textColor="?attr/textColor" | ||||
|         android:textSize="20sp" | ||||
|         android:textStyle="bold" | ||||
|         tools:text="Test" /> | ||||
| 
 | ||||
|     <EditText | ||||
|             android:id="@+id/nginx_text_input" | ||||
|             android:nextFocusRight="@id/cancel_btt" | ||||
|             android:nextFocusLeft="@id/apply_btt" | ||||
|             android:layout_marginBottom="60dp" | ||||
|             android:layout_marginHorizontal="10dp" | ||||
|             android:paddingTop="10dp" | ||||
|             android:requiresFadingEdge="vertical" | ||||
|             tools:text="nginx.com" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="match_parent" | ||||
|             android:layout_rowWeight="1" | ||||
|             android:autofillHints="Autofill Hint" | ||||
|             android:inputType="text" | ||||
|             tools:ignore="LabelFor" /> | ||||
| 
 | ||||
|     <LinearLayout | ||||
|             android:id="@+id/apply_btt_holder" | ||||
|             android:orientation="horizontal" | ||||
|             android:layout_gravity="bottom" | ||||
|             android:gravity="bottom|end" | ||||
|             android:layout_marginTop="-60dp" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="60dp"> | ||||
| 
 | ||||
|         <com.google.android.material.button.MaterialButton | ||||
|                 style="@style/WhiteButton" | ||||
|                 android:layout_gravity="center_vertical|end" | ||||
|                 android:text="@string/sort_apply" | ||||
|                 android:id="@+id/apply_btt" | ||||
|                 android:layout_width="wrap_content" /> | ||||
| 
 | ||||
|         <com.google.android.material.button.MaterialButton | ||||
|                 style="@style/BlackButton" | ||||
|                 android:layout_gravity="center_vertical|end" | ||||
|                 android:text="@string/sort_cancel" | ||||
|                 android:id="@+id/cancel_btt" | ||||
|                 android:layout_width="wrap_content" /> | ||||
|     </LinearLayout> | ||||
| </LinearLayout> | ||||
|  | @ -32,7 +32,7 @@ | |||
|     </array> | ||||
| 
 | ||||
|     <array name="dns_pref"> | ||||
|         <item>None</item> | ||||
|         <item>@string/none</item> | ||||
|         <item>Google</item> | ||||
|         <item>Cloudflare</item> | ||||
|         <!--        <item>OpenDns</item>--> | ||||
|  | @ -59,6 +59,21 @@ | |||
|         <item>3</item> | ||||
|     </array> | ||||
| 
 | ||||
|     <array name="limit_title_pref_names"> | ||||
|         <item>@string/none</item> | ||||
|         <item>16 characters</item> | ||||
|         <item>32 characters</item> | ||||
|         <item>64 characters</item> | ||||
|         <item>128 characters</item> | ||||
|     </array> | ||||
|     <array name="limit_title_pref_values"> | ||||
|         <item>0</item> | ||||
|         <item>16</item> | ||||
|         <item>32</item> | ||||
|         <item>64</item> | ||||
|         <item>128</item> | ||||
|     </array> | ||||
| 
 | ||||
|     <array name="video_buffer_length_names"> | ||||
|         <item>@string/automatic</item> | ||||
|         <item>1min</item> | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
|     <string name="subtitle_settings_key" translatable="false">subtitle_settings_key</string> | ||||
|     <string name="subtitle_settings_chromecast_key" translatable="false">subtitle_settings_chromecast_key</string> | ||||
|     <string name="quality_pref_key" translatable="false">quality_pref_key</string> | ||||
|     <string name="prefer_limit_title_key" translatable="false">prefer_limit_title_key</string> | ||||
|     <string name="video_buffer_size_key" translatable="false">video_buffer_size_key</string> | ||||
|     <string name="video_buffer_length_key" translatable="false">video_buffer_length_key</string> | ||||
|     <string name="video_buffer_clear_key" translatable="false">video_buffer_clear_key</string> | ||||
|  | @ -31,6 +32,9 @@ | |||
|     <string name="provider_lang_key" translatable="false">provider_lang_key</string> | ||||
|     <string name="dns_key" translatable="false">dns_key</string> | ||||
|     <string name="download_path_key" translatable="false">download_path_key</string> | ||||
|     <string name="nginx_url_key" translatable="false">nginx_url_key</string> | ||||
|     <string name="nginx_credentials" translatable="false">nginx_credentials</string> | ||||
|     <string name="nginx_info" translatable="false">nginx_info</string> | ||||
|     <string name="app_name_download_path" translatable="false">Cloudstream</string> | ||||
|     <string name="app_layout_key" translatable="false">app_layout_key</string> | ||||
|     <string name="primary_color_key" translatable="false">primary_color_key</string> | ||||
|  | @ -227,6 +231,12 @@ | |||
|     <string name="backup_failed_error_format">Error backing up %s</string> | ||||
| 
 | ||||
|     <string name="search">Search</string> | ||||
|     <string name="nginx_category">Nginx Settings</string> | ||||
|     <string name="nginx_credentials_title">Nginx Credential</string> | ||||
|     <string name="nginx_credentials_summary">You have to use the following format mycoolusername:mysecurepassword123</string> | ||||
|     <string name="nginx_info_title">What is Nginx ?</string> | ||||
|     <string name="nginx_info_summary">Nginx is a software that can be used to display files from a server that you own. Click to see a Nginx setup guide</string> | ||||
| 
 | ||||
|     <string name="settings_info">Info</string> | ||||
|     <string name="advanced_search">Advanced Search</string> | ||||
|     <string name="advanced_search_des">Gives you the search results separated by provider</string> | ||||
|  | @ -339,6 +349,7 @@ | |||
|     <string name="dont_show_again">Don\'t show again</string> | ||||
|     <string name="update">Update</string> | ||||
|     <string name="watch_quality_pref">Preferred watch quality</string> | ||||
|     <string name="limit_title">Limit title characters on player</string> | ||||
|     <string name="video_buffer_size_settings">Video buffer size</string> | ||||
|     <string name="video_buffer_length_settings">Video buffer length</string> | ||||
|     <string name="video_buffer_disk_settings">Video cache on disk</string> | ||||
|  | @ -352,6 +363,8 @@ | |||
| 
 | ||||
|     <string name="download_path_pref">Download path</string> | ||||
| 
 | ||||
|     <string name="nginx_url_pref">Nginx server url</string> | ||||
| 
 | ||||
|     <string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string> | ||||
| 
 | ||||
|     <string name="resize_fit">Fit to screen</string> | ||||
|  |  | |||
|  | @ -20,6 +20,10 @@ | |||
|                 android:key="@string/quality_pref_key" | ||||
|                 android:title="@string/watch_quality_pref" | ||||
|                 android:icon="@drawable/ic_baseline_hd_24" /> | ||||
|         <Preference | ||||
|             android:key="@string/prefer_limit_title_key" | ||||
|             android:title="@string/limit_title" | ||||
|             android:icon="@drawable/ic_baseline_text_format_24" /> | ||||
| 
 | ||||
|         <SwitchPreference | ||||
|                 android:icon="@drawable/ic_baseline_picture_in_picture_alt_24" | ||||
|  | @ -162,6 +166,31 @@ | |||
|                 app:defaultValue="true" /> | ||||
|     </PreferenceCategory> | ||||
| 
 | ||||
| 
 | ||||
|     <PreferenceCategory | ||||
|         android:key="@string/nginx_category" | ||||
|         android:title="@string/nginx_category" | ||||
|         app:isPreferenceVisible="true"> | ||||
|         <Preference | ||||
|             android:key="@string/nginx_url_key" | ||||
|             android:title="@string/nginx_url_pref" | ||||
|             android:icon="@drawable/ic_baseline_play_arrow_24" /> | ||||
|         <Preference | ||||
|             android:key="@string/nginx_credentials" | ||||
|             android:title="@string/nginx_credentials_title" | ||||
|             android:icon="@drawable/video_locked" | ||||
|             android:summary="@string/nginx_credentials_summary"/> | ||||
|         <Preference | ||||
|             android:key="@string/nginx_info" | ||||
|             android:title="@string/nginx_info_title" | ||||
|             android:icon="@drawable/ic_baseline_play_arrow_24" | ||||
| 	        android:summary="@string/nginx_info_summary" > | ||||
|             <intent | ||||
|                 android:action="android.intent.action.VIEW" | ||||
|                 android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" /> | ||||
|         </Preference> | ||||
|     </PreferenceCategory> | ||||
| 
 | ||||
|     <PreferenceCategory | ||||
|             android:key="info" | ||||
|             android:title="@string/settings_info" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue