forked from recloudstream/cloudstream
		
	Opensubs dev into master (#1136)
* [Feature][WIP] Add option to download subtitles from Opensubtitles.org (#1082) Co-authored-by: Jace <54625750+Jacekun@users.noreply.github.com> Co-authored-by: Jace <jaceorwell@gmail.com> Co-authored-by: LagradOst <11805592+LagradOst@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									941faf7b5d
								
							
						
					
					
						commit
						918136f8f0
					
				
					 46 changed files with 1499 additions and 499 deletions
				
			
		|  | @ -14,8 +14,8 @@ import com.lagradost.cloudstream3.animeproviders.* | ||||||
| import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider | import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider | ||||||
| import com.lagradost.cloudstream3.movieproviders.* | import com.lagradost.cloudstream3.movieproviders.* | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi | ||||||
| import com.lagradost.cloudstream3.ui.player.SubtitleData | import com.lagradost.cloudstream3.ui.player.SubtitleData | ||||||
| import com.lagradost.cloudstream3.utils.AppUtils.toJson | import com.lagradost.cloudstream3.utils.AppUtils.toJson | ||||||
| import com.lagradost.cloudstream3.utils.ExtractorLink | import com.lagradost.cloudstream3.utils.ExtractorLink | ||||||
|  | @ -352,7 +352,8 @@ abstract class MainAPI { | ||||||
| 
 | 
 | ||||||
|     fun overrideWithNewData(data: ProvidersInfoJson) { |     fun overrideWithNewData(data: ProvidersInfoJson) { | ||||||
|         this.name = data.name |         this.name = data.name | ||||||
|         this.mainUrl = data.url |         if (data.url.isNotBlank() && data.url != "NONE") | ||||||
|  |             this.mainUrl = data.url | ||||||
|         this.storedCredentials = data.credentials |         this.storedCredentials = data.credentials | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -35,13 +35,13 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent | ||||||
| import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint | import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint | ||||||
| import com.lagradost.cloudstream3.CommonActivity.showToast | import com.lagradost.cloudstream3.CommonActivity.showToast | ||||||
| import com.lagradost.cloudstream3.CommonActivity.updateLocale | import com.lagradost.cloudstream3.CommonActivity.updateLocale | ||||||
| import com.lagradost.cloudstream3.movieproviders.NginxProvider |  | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.network.initClient | import com.lagradost.cloudstream3.network.initClient | ||||||
| import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver | import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2Apis | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accountApis | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths | ||||||
| import com.lagradost.cloudstream3.ui.APIRepository | import com.lagradost.cloudstream3.ui.APIRepository | ||||||
| import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO | import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO | ||||||
| import com.lagradost.cloudstream3.ui.result.ResultFragment | import com.lagradost.cloudstream3.ui.result.ResultFragment | ||||||
|  | @ -132,7 +132,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
|             R.id.navigation_download_child, |             R.id.navigation_download_child, | ||||||
|             R.id.navigation_subtitles, |             R.id.navigation_subtitles, | ||||||
|             R.id.navigation_chrome_subtitles, |             R.id.navigation_chrome_subtitles, | ||||||
|             R.id.navigation_settings_nginx, |  | ||||||
|             R.id.navigation_settings_player, |             R.id.navigation_settings_player, | ||||||
|             R.id.navigation_settings_updates, |             R.id.navigation_settings_updates, | ||||||
|             R.id.navigation_settings_ui, |             R.id.navigation_settings_ui, | ||||||
|  | @ -368,10 +367,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
| 
 | 
 | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         // init accounts |         // init accounts | ||||||
|         for (api in OAuth2accountApis) { |         for (api in accountManagers) { | ||||||
|             api.init() |             api.init() | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         ioSafe { | ||||||
|  |             inAppAuths.apmap { api -> | ||||||
|  |                 try { | ||||||
|  |                     api.initialize() | ||||||
|  |                 } catch (e: Exception) { | ||||||
|  |                     logError(e) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         SearchResultBuilder.updateCache(this) |         SearchResultBuilder.updateCache(this) | ||||||
| 
 | 
 | ||||||
|         val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) |         val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) | ||||||
|  | @ -391,68 +400,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
|             false |             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 |         // this pulls the latest data so ppl don't have to update to simply change provider url | ||||||
|         if (downloadFromGithub) { |         if (downloadFromGithub) { | ||||||
|             try { |             try { | ||||||
|  | @ -472,11 +419,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
|                                         setKey(PROVIDER_STATUS_KEY, txt) |                                         setKey(PROVIDER_STATUS_KEY, txt) | ||||||
|                                         MainAPI.overrideData = newCache // update all new providers |                                         MainAPI.overrideData = newCache // update all new providers | ||||||
| 
 | 
 | ||||||
|                                         val newUpdatedCache = |  | ||||||
|                                             newCache?.let { addNginxToJson(it) } |  | ||||||
|                                         initAll() |                                         initAll() | ||||||
|                                         for (api in apis) { // update current providers |                                         for (api in apis) { // update current providers | ||||||
|                                             newUpdatedCache?.get(api.javaClass.simpleName) |                                             newCache?.get(api.javaClass.simpleName) | ||||||
|                                                 ?.let { data -> |                                                 ?.let { data -> | ||||||
|                                                     api.overrideWithNewData(data) |                                                     api.overrideWithNewData(data) | ||||||
|                                                 } |                                                 } | ||||||
|  | @ -494,15 +439,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
|                                 newCache |                                 newCache | ||||||
|                             }?.let { providersJsonMap -> |                             }?.let { providersJsonMap -> | ||||||
|                                 MainAPI.overrideData = providersJsonMap |                                 MainAPI.overrideData = providersJsonMap | ||||||
|                                 val providersJsonMapUpdated = |  | ||||||
|                                     addNginxToJson(providersJsonMap) // if return null, use unchanged one |  | ||||||
|                                 initAll() |                                 initAll() | ||||||
|                                 val acceptableProviders = |                                 val acceptableProviders = | ||||||
|                                     providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } |                                     providersJsonMap.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } | ||||||
|                                         .map { it.key }.toSet() |                                         .map { it.key }.toSet() | ||||||
| 
 | 
 | ||||||
|                                 val restrictedApis = |                                 val restrictedApis = | ||||||
|                                     if (hasBenene) providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } |                                     if (hasBenene) providersJsonMap.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } | ||||||
|                                         .map { it.key }.toSet() else emptySet() |                                         .map { it.key }.toSet() else emptySet() | ||||||
| 
 | 
 | ||||||
|                                 apis = allProviders.filter { api -> |                                 apis = allProviders.filter { api -> | ||||||
|  | @ -527,16 +470,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
|         } else { |         } else { | ||||||
|             initAll() |             initAll() | ||||||
|             apis = allProviders |             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) |         loadThemes(this) | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| package com.lagradost.cloudstream3.metaproviders | package com.lagradost.cloudstream3.metaproviders | ||||||
| 
 | 
 | ||||||
| import com.lagradost.cloudstream3.ErrorLoadingException | import com.lagradost.cloudstream3.ErrorLoadingException | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi | ||||||
| import com.lagradost.cloudstream3.utils.SyncUtil | import com.lagradost.cloudstream3.utils.SyncUtil | ||||||
| 
 | 
 | ||||||
| object SyncRedirector { | object SyncRedirector { | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ package com.lagradost.cloudstream3.metaproviders | ||||||
| import com.lagradost.cloudstream3.* | import com.lagradost.cloudstream3.* | ||||||
| import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId | import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId | ||||||
| import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer | import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||||
| import com.lagradost.cloudstream3.syncproviders.providers.AniListApi | import com.lagradost.cloudstream3.syncproviders.providers.AniListApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.providers.MALApi | import com.lagradost.cloudstream3.syncproviders.providers.MALApi | ||||||
|  | @ -15,7 +15,7 @@ class MultiAnimeProvider : MainAPI() { | ||||||
|     override val lang = "en" |     override val lang = "en" | ||||||
|     override val usesWebView = true |     override val usesWebView = true | ||||||
|     override val supportedTypes = setOf(TvType.Anime) |     override val supportedTypes = setOf(TvType.Anime) | ||||||
|     private val syncApi: SyncAPI = OAuth2API.aniListApi |     private val syncApi: SyncAPI = aniListApi | ||||||
| 
 | 
 | ||||||
|     private val syncUtilType by lazy { |     private val syncUtilType by lazy { | ||||||
|         when (syncApi) { |         when (syncApi) { | ||||||
|  |  | ||||||
|  | @ -161,7 +161,6 @@ class EgyBestProvider : MainAPI() { | ||||||
|         @JsonProperty("link") val link: String |         @JsonProperty("link") val link: String | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     override suspend fun loadLinks( |     override suspend fun loadLinks( | ||||||
|         data: String, |         data: String, | ||||||
|         isCasting: Boolean, |         isCasting: Boolean, | ||||||
|  |  | ||||||
|  | @ -1,13 +1,8 @@ | ||||||
| package com.lagradost.cloudstream3.movieproviders | package com.lagradost.cloudstream3.movieproviders | ||||||
| 
 | 
 | ||||||
| import com.lagradost.cloudstream3.* | import com.lagradost.cloudstream3.* | ||||||
| import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration |  | ||||||
| import com.lagradost.cloudstream3.LoadResponse.Companion.addRating |  | ||||||
| import com.lagradost.cloudstream3.utils.ExtractorLink | import com.lagradost.cloudstream3.utils.ExtractorLink | ||||||
| import com.lagradost.cloudstream3.TvType |  | ||||||
| import com.lagradost.cloudstream3.app |  | ||||||
| import com.lagradost.cloudstream3.utils.Qualities | import com.lagradost.cloudstream3.utils.Qualities | ||||||
| import java.lang.Exception |  | ||||||
| 
 | 
 | ||||||
| class NginxProvider : MainAPI() { | class NginxProvider : MainAPI() { | ||||||
|     override var name = "Nginx" |     override var name = "Nginx" | ||||||
|  | @ -15,23 +10,40 @@ class NginxProvider : MainAPI() { | ||||||
|     override val hasMainPage = true |     override val hasMainPage = true | ||||||
|     override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) |     override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) | ||||||
| 
 | 
 | ||||||
|  |     companion object { | ||||||
|  |         var loginCredentials: String? = null | ||||||
|  |         var overrideUrl: String? = null | ||||||
|  |         const val ERROR_STRING = "No nginx url specified in the settings" | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 
 |     private fun getAuthHeader(): Map<String, String> { | ||||||
|     fun getAuthHeader(storedCredentials: String?): Map<String, String> { |         val url = overrideUrl ?: throw ErrorLoadingException(ERROR_STRING) | ||||||
|         if (storedCredentials == null) { |         mainUrl = url | ||||||
|             return mapOf(Pair("Authorization", "Basic "))  // no Authorization headers |         println("OVERRIDING URL TO $overrideUrl") | ||||||
|  |         if (mainUrl == "NONE" || mainUrl.isBlank()) { | ||||||
|  |             throw ErrorLoadingException(ERROR_STRING) | ||||||
|         } |         } | ||||||
|         val basicAuthToken = base64Encode(storedCredentials.toByteArray())  // will this be loaded when not using the provider ??? can increase load | 
 | ||||||
|         return mapOf(Pair("Authorization", "Basic $basicAuthToken")) |         val localCredentials = loginCredentials | ||||||
|  |         if (localCredentials == null || localCredentials.trim() == ":") { | ||||||
|  |             return mapOf("Authorization" to "Basic ")  // no Authorization headers | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val basicAuthToken = | ||||||
|  |             base64Encode(localCredentials.toByteArray())  // will this be loaded when not using the provider ??? can increase load | ||||||
|  | 
 | ||||||
|  |         return mapOf("Authorization" to "Basic $basicAuthToken") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun load(url: String): LoadResponse { |     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 |         val authHeader = | ||||||
|  |             getAuthHeader()  // 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 |         // url can be tvshow.nfo for series or mediaRootUrl for movies | ||||||
| 
 | 
 | ||||||
|         val mediaRootDocument = app.get(url, authHeader).document |         val mainRootDocument = app.get(url, authHeader).document | ||||||
| 
 | 
 | ||||||
|         val nfoUrl = url + mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href")  // metadata url file |         val nfoUrl = url + mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo") | ||||||
|  |             .attr("href")  // metadata url file | ||||||
| 
 | 
 | ||||||
|         val metadataDocument = app.get(nfoUrl, authHeader).document  // get the metadata nfo file |         val metadataDocument = app.get(nfoUrl, authHeader).document  // get the metadata nfo file | ||||||
| 
 | 
 | ||||||
|  | @ -44,27 +56,34 @@ class NginxProvider : MainAPI() { | ||||||
|         if (isMovie) { |         if (isMovie) { | ||||||
|             val poster = metadataDocument.selectFirst("thumb")!!.text() |             val poster = metadataDocument.selectFirst("thumb")!!.text() | ||||||
|             val trailer = metadataDocument.select("trailer").mapNotNull { |             val trailer = metadataDocument.select("trailer").mapNotNull { | ||||||
|                it?.text()?.replace( |                 it?.text()?.replace( | ||||||
|                    "plugin://plugin.video.youtube/play/?video_id=", |                     "plugin://plugin.video.youtube/play/?video_id=", | ||||||
|                    "https://www.youtube.com/watch?v=" |                     "https://www.youtube.com/watch?v=" | ||||||
|                ) |                 ) | ||||||
|             } |             } | ||||||
|             val partialUrl = mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href").replace(".nfo", ".") |             val partialUrl = | ||||||
|  |                 mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href") | ||||||
|  |                     .replace(".nfo", ".") | ||||||
|             val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull() |             val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull() | ||||||
|             val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull() |             val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull() | ||||||
|             val tagsList = metadataDocument.select("genre") |             val tagsList = metadataDocument.select("genre") | ||||||
|                 ?.mapNotNull {   // all the tags like action, thriller ... |                 .mapNotNull {   // all the tags like action, thriller ... | ||||||
|                     it?.text() |                     it?.text() | ||||||
| 
 | 
 | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|             val dataList = mediaRootDocument.getElementsByAttributeValueContaining(  // list of all urls of the webpage |             val dataList = | ||||||
|                 "href", |                 mainRootDocument.getElementsByAttributeValueContaining(  // list of all urls of the webpage | ||||||
|                 partialUrl |                     "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 |             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 newMovieLoadResponse( |             return newMovieLoadResponse( | ||||||
|                 title, |                 title, | ||||||
|  | @ -81,7 +100,6 @@ class NginxProvider : MainAPI() { | ||||||
|             } |             } | ||||||
|         } else  // a tv serie |         } else  // a tv serie | ||||||
|         { |         { | ||||||
| 
 |  | ||||||
|             val list = ArrayList<Pair<Int, String>>() |             val list = ArrayList<Pair<Int, String>>() | ||||||
|             val mediaRootUrl = url.replace("tvshow.nfo", "") |             val mediaRootUrl = url.replace("tvshow.nfo", "") | ||||||
|             val posterUrl = mediaRootUrl + "poster.jpg" |             val posterUrl = mediaRootUrl + "poster.jpg" | ||||||
|  | @ -91,7 +109,7 @@ class NginxProvider : MainAPI() { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|             val tagsList = metadataDocument.select("genre") |             val tagsList = metadataDocument.select("genre") | ||||||
|                 ?.mapNotNull {   // all the tags like action, thriller ...; unused variable |                 .mapNotNull {   // all the tags like action, thriller ...; unused variable | ||||||
|                     it?.text() |                     it?.text() | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  | @ -102,7 +120,7 @@ class NginxProvider : MainAPI() { | ||||||
| 
 | 
 | ||||||
|             seasons.forEach { element -> |             seasons.forEach { element -> | ||||||
|                 val season = |                 val season = | ||||||
|                     element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull() |                     element.attr("href").replace("Season%20", "").replace("/", "").toIntOrNull() | ||||||
|                 val href = mediaRootUrl + element.attr("href") |                 val href = mediaRootUrl + element.attr("href") | ||||||
|                 if (season != null && season > 0 && href.isNotBlank()) { |                 if (season != null && season > 0 && href.isNotBlank()) { | ||||||
|                     list.add(Pair(season, href)) |                     list.add(Pair(season, href)) | ||||||
|  | @ -120,33 +138,40 @@ class NginxProvider : MainAPI() { | ||||||
|                     "href", |                     "href", | ||||||
|                     ".nfo" |                     ".nfo" | ||||||
|                 ) // get metadata |                 ) // get metadata | ||||||
|                     episodes.forEach { episode -> |                 episodes.forEach { episode -> | ||||||
|                         val nfoDocument = app.get(seasonString + episode.attr("href"), authHeader).document // get episode metadata file |                     val nfoDocument = app.get( | ||||||
|                         val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull() |                         seasonString + episode.attr("href"), | ||||||
|                         val poster = |                         authHeader | ||||||
|                             seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg") |                     ).document // get episode metadata file | ||||||
|                         val name = nfoDocument.selectFirst("title")!!.text() |                     val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull() | ||||||
|                         // val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull() |                     val poster = | ||||||
|                         val date = nfoDocument.selectFirst("aired")?.text() |                         seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg") | ||||||
|                         val plot = nfoDocument.selectFirst("plot")?.text() |                     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( |                     val dataList = seasonDocument.getElementsByAttributeValueContaining( | ||||||
|                             "href", |                         "href", | ||||||
|                             episode.attr("href").replace(".nfo", "") |                         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 |                     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( |                     episodeList.add( | ||||||
|                             newEpisode(data) { |                         newEpisode(data) { | ||||||
|                                     this.name = name |                             this.name = name | ||||||
|                                     this.season = seasonInt |                             this.season = seasonInt | ||||||
|                                     this.episode = epNum |                             this.episode = epNum | ||||||
|                                     this.posterUrl = poster  // will require headers too |                             this.posterUrl = poster  // will require headers too | ||||||
|                                     this.description = plot |                             this.description = plot | ||||||
|                                     addDate(date) |                             addDate(date) | ||||||
|                             } |                         } | ||||||
|                         ) |                     ) | ||||||
|                     } |                 } | ||||||
|             } |             } | ||||||
|             return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) { |             return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) { | ||||||
|                 this.name = title |                 this.name = title | ||||||
|  | @ -168,8 +193,9 @@ class NginxProvider : MainAPI() { | ||||||
|         callback: (ExtractorLink) -> Unit |         callback: (ExtractorLink) -> Unit | ||||||
|     ): Boolean { |     ): Boolean { | ||||||
|         // loadExtractor(data, null) { callback(it.copy(headers=authHeader)) } |         // 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 |         val authHeader = | ||||||
|         callback.invoke ( |             getAuthHeader()  // call again because it isn't reloaded if in main class and storedCredentials loads after | ||||||
|  |         callback.invoke( | ||||||
|             ExtractorLink( |             ExtractorLink( | ||||||
|                 name, |                 name, | ||||||
|                 name, |                 name, | ||||||
|  | @ -185,19 +211,23 @@ class NginxProvider : MainAPI() { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     override suspend fun getMainPage(): HomePageResponse { |     override suspend fun getMainPage(): HomePageResponse { | ||||||
|         val authHeader = getAuthHeader(storedCredentials)  // call again because it isn't reloaded if in main class and storedCredentials loads after |         val authHeader = | ||||||
|         if (mainUrl == "NONE"){ |             getAuthHeader()  // call again because it isn't reloaded if in main class and storedCredentials loads after | ||||||
|             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 document = app.get(mainUrl, authHeader).document | ||||||
|         val categories = document.select("a") |         val categories = document.select("a") | ||||||
|         val returnList = categories.mapNotNull { |         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 |             val categoryTitle = it.text()  // get the category title like Movies or Series | ||||||
|             if (categoryTitle != "../" && categoryTitle != "Music/") {  // exclude parent dir and Music dir |             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 href = it?.attr("href") | ||||||
|  |                 val categoryPath = fixUrlNull(href?.trim()) | ||||||
|  |                     ?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/ | ||||||
|  | 
 | ||||||
|  |                 val categoryDocument = app.get( | ||||||
|  |                     categoryPath, | ||||||
|  |                     authHeader | ||||||
|  |                 ).document // queries the page http://192.168.1.10/media/Movies/ | ||||||
|                 val contentLinks = categoryDocument.select("a") |                 val contentLinks = categoryDocument.select("a") | ||||||
|                 val currentList = contentLinks.mapNotNull { head -> |                 val currentList = contentLinks.mapNotNull { head -> | ||||||
|                     if (head.attr("href") != "../") { |                     if (head.attr("href") != "../") { | ||||||
|  | @ -215,7 +245,6 @@ class NginxProvider : MainAPI() { | ||||||
|                             val nfoContent = |                             val nfoContent = | ||||||
|                                 app.get(nfoPath, authHeader).document  // all the metadata |                                 app.get(nfoPath, authHeader).document  // all the metadata | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|                             if (isMovieType) { |                             if (isMovieType) { | ||||||
|                                 val movieName = nfoContent.select("title").text() |                                 val movieName = nfoContent.select("title").text() | ||||||
|                                 val posterUrl = mediaRootUrl + "poster.jpg" |                                 val posterUrl = mediaRootUrl + "poster.jpg" | ||||||
|  | @ -238,15 +267,11 @@ class NginxProvider : MainAPI() { | ||||||
|                                 ) { |                                 ) { | ||||||
|                                     addPoster(posterUrl, authHeader) |                                     addPoster(posterUrl, authHeader) | ||||||
|                                 } |                                 } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                             } |                             } | ||||||
|                         } catch (e: Exception) {  // can cause issues invisible errors |                         } catch (e: Exception) {  // can cause issues invisible errors | ||||||
|                             null |                             null | ||||||
|                             //logError(e) // not working because it changes the return type of currentList to Any |                             //logError(e) // not working because it changes the return type of currentList to Any | ||||||
|                         } |                         } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                     } else null |                     } else null | ||||||
|                 } |                 } | ||||||
|                 if (currentList.isNotEmpty() && categoryTitle != "../") {  // exclude upper dir |                 if (currentList.isNotEmpty() && categoryTitle != "../") {  // exclude upper dir | ||||||
|  |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | package com.lagradost.cloudstream3.subtitles | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.WorkerThread | ||||||
|  | import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity | ||||||
|  | import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch | ||||||
|  | 
 | ||||||
|  | interface AbstractSubProvider { | ||||||
|  |     @WorkerThread | ||||||
|  |     suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? { | ||||||
|  |         throw NotImplementedError() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @WorkerThread | ||||||
|  |     suspend fun load(data: SubtitleEntity): String? { | ||||||
|  |         throw NotImplementedError() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | package com.lagradost.cloudstream3.subtitles | ||||||
|  | 
 | ||||||
|  | import com.lagradost.cloudstream3.TvType | ||||||
|  | 
 | ||||||
|  | class AbstractSubtitleEntities { | ||||||
|  |     data class SubtitleEntity( | ||||||
|  |         var idPrefix : String, | ||||||
|  |         var name: String = "", //Title of movie/series. This is the one to be displayed when choosing. | ||||||
|  |         var lang: String = "en", | ||||||
|  |         var data: String = "", //Id or link, depends on provider how to process | ||||||
|  |         var type: TvType = TvType.Movie, //Movie, TV series, etc.. | ||||||
|  |         var epNumber: Int? = null, | ||||||
|  |         var seasonNumber: Int? = null, | ||||||
|  |         var year: Int? = null | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     data class SubtitleSearch( | ||||||
|  |         var query: String = "", | ||||||
|  |         var imdb: Long? = null, | ||||||
|  |         var lang: String? = null, | ||||||
|  |         var epNumber: Int? = null, | ||||||
|  |         var seasonNumber: Int? = null, | ||||||
|  |         var year: Int? = null | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | @ -3,9 +3,77 @@ package com.lagradost.cloudstream3.syncproviders | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys | import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.providers.AniListApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.providers.MALApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.providers.NginxApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
|  | 
 | ||||||
|  | abstract class AccountManager(private val defIndex: Int) : AuthAPI { | ||||||
|  |     companion object { | ||||||
|  |         val malApi = MALApi(0) | ||||||
|  |         val aniListApi = AniListApi(0) | ||||||
|  |         val openSubtitlesApi = OpenSubtitlesApi(0) | ||||||
|  |         val nginxApi = NginxApi(0) | ||||||
|  | 
 | ||||||
|  |         // used to login via app intent | ||||||
|  |         val OAuth2Apis | ||||||
|  |             get() = listOf<OAuth2API>( | ||||||
|  |                 malApi, aniListApi | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         // this needs init with context and can be accessed in settings | ||||||
|  |         val accountManagers | ||||||
|  |             get() = listOf( | ||||||
|  |                 malApi, aniListApi, openSubtitlesApi, nginxApi | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         // used for active syncing | ||||||
|  |         val SyncApis | ||||||
|  |             get() = listOf( | ||||||
|  |                 SyncRepo(malApi), SyncRepo(aniListApi) | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         val inAppAuths | ||||||
|  |             get() = listOf(openSubtitlesApi, nginxApi) | ||||||
|  | 
 | ||||||
|  |         val subtitleProviders | ||||||
|  |             get() = listOf( | ||||||
|  |                 openSubtitlesApi | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         const val appString = "cloudstreamapp" | ||||||
|  | 
 | ||||||
|  |         val unixTime: Long | ||||||
|  |             get() = System.currentTimeMillis() / 1000L | ||||||
|  |         val unixTimeMs: Long | ||||||
|  |             get() = System.currentTimeMillis() | ||||||
|  | 
 | ||||||
|  |         const val maxStale = 60 * 10 | ||||||
|  | 
 | ||||||
|  |         fun secondsToReadable(seconds: Int, completedValue: String): String { | ||||||
|  |             var secondsLong = seconds.toLong() | ||||||
|  |             val days = TimeUnit.SECONDS | ||||||
|  |                 .toDays(secondsLong) | ||||||
|  |             secondsLong -= TimeUnit.DAYS.toSeconds(days) | ||||||
|  | 
 | ||||||
|  |             val hours = TimeUnit.SECONDS | ||||||
|  |                 .toHours(secondsLong) | ||||||
|  |             secondsLong -= TimeUnit.HOURS.toSeconds(hours) | ||||||
|  | 
 | ||||||
|  |             val minutes = TimeUnit.SECONDS | ||||||
|  |                 .toMinutes(secondsLong) | ||||||
|  |             secondsLong -= TimeUnit.MINUTES.toSeconds(minutes) | ||||||
|  |             if (minutes < 0) { | ||||||
|  |                 return completedValue | ||||||
|  |             } | ||||||
|  |             //println("$days $hours $minutes") | ||||||
|  |             return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m" | ||||||
|  |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| abstract class AccountManager(private val defIndex: Int) : OAuth2API { |  | ||||||
|     var accountIndex = defIndex |     var accountIndex = defIndex | ||||||
|  |     private var lastAccountIndex = defIndex | ||||||
|     protected val accountId get() = "${idPrefix}_account_$accountIndex" |     protected val accountId get() = "${idPrefix}_account_$accountIndex" | ||||||
|     private val accountActiveKey get() = "${idPrefix}_active" |     private val accountActiveKey get() = "${idPrefix}_active" | ||||||
| 
 | 
 | ||||||
|  | @ -35,8 +103,12 @@ abstract class AccountManager(private val defIndex: Int) : OAuth2API { | ||||||
| 
 | 
 | ||||||
|     protected fun switchToNewAccount() { |     protected fun switchToNewAccount() { | ||||||
|         val accounts = getAccounts() |         val accounts = getAccounts() | ||||||
|  |         lastAccountIndex = accountIndex | ||||||
|         accountIndex = (accounts?.maxOrNull() ?: 0) + 1 |         accountIndex = (accounts?.maxOrNull() ?: 0) + 1 | ||||||
|     } |     } | ||||||
|  |     protected fun switchToOldAccount() { | ||||||
|  |         accountIndex = lastAccountIndex | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     protected fun registerAccount() { |     protected fun registerAccount() { | ||||||
|         setKey(accountActiveKey, accountIndex) |         setKey(accountActiveKey, accountIndex) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | package com.lagradost.cloudstream3.syncproviders | ||||||
|  | 
 | ||||||
|  | interface AuthAPI { | ||||||
|  |     val name: String | ||||||
|  |     val icon: Int? | ||||||
|  | 
 | ||||||
|  |     val requiresLogin: Boolean | ||||||
|  | 
 | ||||||
|  |     val createAccountUrl : String? | ||||||
|  | 
 | ||||||
|  |     // don't change this as all keys depend on it | ||||||
|  |     val idPrefix: String | ||||||
|  | 
 | ||||||
|  |     // if this returns null then you are not logged in | ||||||
|  |     fun loginInfo(): LoginInfo? | ||||||
|  |     fun logOut() | ||||||
|  | 
 | ||||||
|  |     class LoginInfo( | ||||||
|  |         val profilePicture: String? = null, | ||||||
|  |         val name: String?, | ||||||
|  |         val accountIndex: Int, | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,66 @@ | ||||||
|  | package com.lagradost.cloudstream3.syncproviders | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.WorkerThread | ||||||
|  | 
 | ||||||
|  | interface InAppAuthAPI : AuthAPI { | ||||||
|  |     data class LoginData( | ||||||
|  |         val username: String? = null, | ||||||
|  |         val password: String? = null, | ||||||
|  |         val server: String? = null, | ||||||
|  |         val email: String? = null, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     // this is for displaying the UI | ||||||
|  |     val requiresPassword: Boolean | ||||||
|  |     val requiresUsername: Boolean | ||||||
|  |     val requiresServer: Boolean | ||||||
|  |     val requiresEmail: Boolean | ||||||
|  | 
 | ||||||
|  |     // if this is false we can assume that getLatestLoginData returns null and wont be called | ||||||
|  |     // this is used in case for some reason it is not preferred to store any login data besides the "token" or encrypted data | ||||||
|  |     val storesPasswordInPlainText: Boolean | ||||||
|  | 
 | ||||||
|  |     // return true if logged in successfully | ||||||
|  |     suspend fun login(data: LoginData): Boolean | ||||||
|  | 
 | ||||||
|  |     // used to fill the UI if you want to edit any data about your login info | ||||||
|  |     fun getLatestLoginData(): LoginData? | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | abstract class InAppAuthAPIManager(defIndex: Int) : AccountManager(defIndex), InAppAuthAPI { | ||||||
|  |     override val requiresPassword = false | ||||||
|  |     override val requiresUsername = false | ||||||
|  |     override val requiresEmail = false | ||||||
|  |     override val requiresServer = false | ||||||
|  |     override val storesPasswordInPlainText = true | ||||||
|  |     override val requiresLogin = true | ||||||
|  | 
 | ||||||
|  |     // runs on startup | ||||||
|  |     @WorkerThread | ||||||
|  |     open suspend fun initialize() { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun logOut() { | ||||||
|  |         throw NotImplementedError() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override val idPrefix: String | ||||||
|  |         get() = throw NotImplementedError() | ||||||
|  | 
 | ||||||
|  |     override val name: String | ||||||
|  |         get() = throw NotImplementedError() | ||||||
|  | 
 | ||||||
|  |     override val icon: Int? = null | ||||||
|  | 
 | ||||||
|  |     override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { | ||||||
|  |         throw NotImplementedError() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getLatestLoginData(): InAppAuthAPI.LoginData? { | ||||||
|  |         throw NotImplementedError() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun loginInfo(): AuthAPI.LoginInfo? { | ||||||
|  |         throw NotImplementedError() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,77 +1,9 @@ | ||||||
| package com.lagradost.cloudstream3.syncproviders | package com.lagradost.cloudstream3.syncproviders | ||||||
| 
 | 
 | ||||||
| import com.lagradost.cloudstream3.syncproviders.providers.AniListApi | interface OAuth2API : AuthAPI { | ||||||
| import com.lagradost.cloudstream3.syncproviders.providers.MALApi |  | ||||||
| import java.util.concurrent.TimeUnit |  | ||||||
| 
 |  | ||||||
| interface OAuth2API { |  | ||||||
|     val key: String |     val key: String | ||||||
|     val name: String |  | ||||||
|     val redirectUrl: String |     val redirectUrl: String | ||||||
| 
 | 
 | ||||||
|     // don't change this as all keys depend on it |  | ||||||
|     val idPrefix: String |  | ||||||
| 
 |  | ||||||
|     suspend fun handleRedirect(url: String) : Boolean |     suspend fun handleRedirect(url: String) : Boolean | ||||||
|     fun authenticate() |     fun authenticate() | ||||||
| 
 |  | ||||||
|     fun loginInfo(): LoginInfo? |  | ||||||
|     fun logOut() |  | ||||||
| 
 |  | ||||||
|     class LoginInfo( |  | ||||||
|         val profilePicture: String?, |  | ||||||
|         val name: String?, |  | ||||||
| 
 |  | ||||||
|         val accountIndex: Int, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         val malApi = MALApi(0) |  | ||||||
|         val aniListApi = AniListApi(0) |  | ||||||
| 
 |  | ||||||
|         // used to login via app intent |  | ||||||
|         val OAuth2Apis |  | ||||||
|             get() = listOf<OAuth2API>( |  | ||||||
|                 malApi, aniListApi |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|         // this needs init with context and can be accessed in settings |  | ||||||
|         val OAuth2accountApis |  | ||||||
|             get() = listOf<AccountManager>( |  | ||||||
|                 malApi, aniListApi |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|         // used for active syncing |  | ||||||
|         val SyncApis |  | ||||||
|             get() = listOf( |  | ||||||
|                 SyncRepo(malApi), SyncRepo(aniListApi) |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|         const val appString = "cloudstreamapp" |  | ||||||
| 
 |  | ||||||
|         val unixTime: Long |  | ||||||
|             get() = System.currentTimeMillis() / 1000L |  | ||||||
| 
 |  | ||||||
|         const val maxStale = 60 * 10 |  | ||||||
| 
 |  | ||||||
|         fun secondsToReadable(seconds: Int, completedValue: String): String { |  | ||||||
|             var secondsLong = seconds.toLong() |  | ||||||
|             val days = TimeUnit.SECONDS |  | ||||||
|                 .toDays(secondsLong) |  | ||||||
|             secondsLong -= TimeUnit.DAYS.toSeconds(days) |  | ||||||
| 
 |  | ||||||
|             val hours = TimeUnit.SECONDS |  | ||||||
|                 .toHours(secondsLong) |  | ||||||
|             secondsLong -= TimeUnit.HOURS.toSeconds(hours) |  | ||||||
| 
 |  | ||||||
|             val minutes = TimeUnit.SECONDS |  | ||||||
|                 .toMinutes(secondsLong) |  | ||||||
|             secondsLong -= TimeUnit.MINUTES.toSeconds(minutes) |  | ||||||
|             if (minutes < 0) { |  | ||||||
|                 return completedValue |  | ||||||
|             } |  | ||||||
|             //println("$days $hours $minutes") |  | ||||||
|             return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | @ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders | ||||||
| import com.lagradost.cloudstream3.* | import com.lagradost.cloudstream3.* | ||||||
| 
 | 
 | ||||||
| interface SyncAPI : OAuth2API { | interface SyncAPI : OAuth2API { | ||||||
|     val icon: Int |  | ||||||
|     val mainUrl: String |     val mainUrl: String | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -12,10 +12,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager | import com.lagradost.cloudstream3.syncproviders.AccountManager | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString |  | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.maxStale |  | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime |  | ||||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||||
| import com.lagradost.cloudstream3.utils.AppUtils.splitQuery | import com.lagradost.cloudstream3.utils.AppUtils.splitQuery | ||||||
| import com.lagradost.cloudstream3.utils.AppUtils.toJson | import com.lagradost.cloudstream3.utils.AppUtils.toJson | ||||||
|  | @ -32,11 +29,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     override val idPrefix = "anilist" |     override val idPrefix = "anilist" | ||||||
|     override var mainUrl = "https://anilist.co" |     override var mainUrl = "https://anilist.co" | ||||||
|     override val icon = R.drawable.ic_anilist_icon |     override val icon = R.drawable.ic_anilist_icon | ||||||
|  |     override val requiresLogin = true | ||||||
|  |     override val createAccountUrl = "$mainUrl/signup" | ||||||
| 
 | 
 | ||||||
|     override fun loginInfo(): OAuth2API.LoginInfo? { |     override fun loginInfo(): AuthAPI.LoginInfo? { | ||||||
|         // context.getUser(true)?. |         // context.getUser(true)?. | ||||||
|         getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user -> |         getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user -> | ||||||
|             return OAuth2API.LoginInfo( |             return AuthAPI.LoginInfo( | ||||||
|                 profilePicture = user.picture, |                 profilePicture = user.picture, | ||||||
|                 name = user.name, |                 name = user.name, | ||||||
|                 accountIndex = accountIndex |                 accountIndex = accountIndex | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| package com.lagradost.cloudstream3.syncproviders.providers | package com.lagradost.cloudstream3.syncproviders.providers | ||||||
| 
 | 
 | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.OAuth2API | ||||||
| 
 | 
 | ||||||
| //TODO dropbox sync | //TODO dropbox sync | ||||||
|  | @ -8,6 +9,11 @@ class Dropbox : OAuth2API { | ||||||
|     override var name = "Dropbox" |     override var name = "Dropbox" | ||||||
|     override val key = "zlqsamadlwydvb2" |     override val key = "zlqsamadlwydvb2" | ||||||
|     override val redirectUrl = "dropboxlogin" |     override val redirectUrl = "dropboxlogin" | ||||||
|  |     override val requiresLogin = true | ||||||
|  |     override val createAccountUrl: String? = null | ||||||
|  | 
 | ||||||
|  |     override val icon: Int | ||||||
|  |         get() = TODO("Not yet implemented") | ||||||
| 
 | 
 | ||||||
|     override fun authenticate() { |     override fun authenticate() { | ||||||
|         TODO("Not yet implemented") |         TODO("Not yet implemented") | ||||||
|  | @ -21,7 +27,7 @@ class Dropbox : OAuth2API { | ||||||
|         TODO("Not yet implemented") |         TODO("Not yet implemented") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun loginInfo(): OAuth2API.LoginInfo? { |     override fun loginInfo(): AuthAPI.LoginInfo? { | ||||||
|         TODO("Not yet implemented") |         TODO("Not yet implemented") | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -14,10 +14,7 @@ import com.lagradost.cloudstream3.ShowStatus | ||||||
| import com.lagradost.cloudstream3.app | import com.lagradost.cloudstream3.app | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager | import com.lagradost.cloudstream3.syncproviders.AccountManager | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString |  | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable |  | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime |  | ||||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||||
| import com.lagradost.cloudstream3.utils.AppUtils.splitQuery | import com.lagradost.cloudstream3.utils.AppUtils.splitQuery | ||||||
| import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject | import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject | ||||||
|  | @ -37,15 +34,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     override val idPrefix = "mal" |     override val idPrefix = "mal" | ||||||
|     override var mainUrl = "https://myanimelist.net" |     override var mainUrl = "https://myanimelist.net" | ||||||
|     override val icon = R.drawable.mal_logo |     override val icon = R.drawable.mal_logo | ||||||
|  |     override val requiresLogin = true | ||||||
|  | 
 | ||||||
|  |     override val createAccountUrl = "$mainUrl/register.php" | ||||||
| 
 | 
 | ||||||
|     override fun logOut() { |     override fun logOut() { | ||||||
|         removeAccountKeys() |         removeAccountKeys() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun loginInfo(): OAuth2API.LoginInfo? { |     override fun loginInfo(): AuthAPI.LoginInfo? { | ||||||
|         //getMalUser(true)? |         //getMalUser(true)? | ||||||
|         getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user -> |         getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user -> | ||||||
|             return OAuth2API.LoginInfo( |             return AuthAPI.LoginInfo( | ||||||
|                 profilePicture = user.picture, |                 profilePicture = user.picture, | ||||||
|                 name = user.name, |                 name = user.name, | ||||||
|                 accountIndex = accountIndex |                 accountIndex = accountIndex | ||||||
|  |  | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | package com.lagradost.cloudstream3.syncproviders.providers | ||||||
|  | 
 | ||||||
|  | import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||||
|  | import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
|  | import com.lagradost.cloudstream3.R | ||||||
|  | import com.lagradost.cloudstream3.movieproviders.NginxProvider | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager | ||||||
|  | 
 | ||||||
|  | class NginxApi(index: Int) : InAppAuthAPIManager(index) { | ||||||
|  |     override val name = "Nginx" | ||||||
|  |     override val idPrefix = "nginx" | ||||||
|  |     override val icon = R.drawable.nginx | ||||||
|  |     override val requiresUsername = true | ||||||
|  |     override val requiresPassword = true | ||||||
|  |     override val requiresServer = true | ||||||
|  |     override val createAccountUrl = "https://www.sarlays.com/use-nginx-with-cloudstream/" | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         const val NGINX_USER_KEY: String = "nginx_user" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getLatestLoginData(): InAppAuthAPI.LoginData? { | ||||||
|  |         return getKey(accountId, NGINX_USER_KEY) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun loginInfo(): AuthAPI.LoginInfo? { | ||||||
|  |         val data = getLatestLoginData() ?: return null | ||||||
|  |         return AuthAPI.LoginInfo(name = data.username ?: data.server, accountIndex = accountIndex) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { | ||||||
|  |         if (data.server.isNullOrBlank()) return false // we require a server | ||||||
|  |         switchToNewAccount() | ||||||
|  |         setKey(accountId, NGINX_USER_KEY, data) | ||||||
|  |         registerAccount() | ||||||
|  |         initialize() | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun logOut() { | ||||||
|  |         removeAccountKeys() | ||||||
|  |         initializeData() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun initializeData() { | ||||||
|  |         val data = getLatestLoginData() ?: run { | ||||||
|  |             NginxProvider.overrideUrl = null | ||||||
|  |             NginxProvider.loginCredentials = null | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         NginxProvider.overrideUrl = data.server?.removeSuffix("/") | ||||||
|  |         NginxProvider.loginCredentials = "${data.username ?: ""}:${data.password ?: ""}" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun initialize() { | ||||||
|  |         initializeData() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,311 @@ | ||||||
|  | package com.lagradost.cloudstream3.syncproviders.providers | ||||||
|  | 
 | ||||||
|  | import android.util.Log | ||||||
|  | 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.ErrorLoadingException | ||||||
|  | import com.lagradost.cloudstream3.R | ||||||
|  | import com.lagradost.cloudstream3.TvType | ||||||
|  | import com.lagradost.cloudstream3.app | ||||||
|  | import com.lagradost.cloudstream3.mvvm.logError | ||||||
|  | import com.lagradost.cloudstream3.subtitles.AbstractSubProvider | ||||||
|  | import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager | ||||||
|  | import com.lagradost.cloudstream3.utils.AppUtils | ||||||
|  | 
 | ||||||
|  | class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubProvider { | ||||||
|  |     override val idPrefix = "opensubtitles" | ||||||
|  |     override val name = "OpenSubtitles" | ||||||
|  |     override val icon = R.drawable.open_subtitles_icon | ||||||
|  |     override val requiresPassword = true | ||||||
|  |     override val requiresUsername = true | ||||||
|  |     override val createAccountUrl = "https://www.opensubtitles.com/" | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile | ||||||
|  |         const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" | ||||||
|  |         const val host = "https://api.opensubtitles.com/api/v1" | ||||||
|  |         const val TAG = "OPENSUBS" | ||||||
|  |         const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms | ||||||
|  |         var currentCoolDown: Long = 0L | ||||||
|  |         var currentSession: SubtitleOAuthEntity? = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun canDoRequest(): Boolean { | ||||||
|  |         return unixTimeMs > currentCoolDown | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun throwIfCantDoRequest() { | ||||||
|  |         if (!canDoRequest()) { | ||||||
|  |             throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMs) / 1000L}s") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun throwGotTooManyRequests() { | ||||||
|  |         currentCoolDown = unixTimeMs + coolDownDuration | ||||||
|  |         throw ErrorLoadingException("Too many requests") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun getAuthKey(): SubtitleOAuthEntity? { | ||||||
|  |         return getKey(accountId, OPEN_SUBTITLES_USER_KEY) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setAuthKey(data: SubtitleOAuthEntity?) { | ||||||
|  |         if (data == null) removeKey(accountId, OPEN_SUBTITLES_USER_KEY) | ||||||
|  |         currentSession = data | ||||||
|  |         setKey(accountId, OPEN_SUBTITLES_USER_KEY, data) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun loginInfo(): AuthAPI.LoginInfo? { | ||||||
|  |         getAuthKey()?.let { user -> | ||||||
|  |             return AuthAPI.LoginInfo( | ||||||
|  |                 profilePicture = null, | ||||||
|  |                 name = user.user, | ||||||
|  |                 accountIndex = accountIndex | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getLatestLoginData(): InAppAuthAPI.LoginData? { | ||||||
|  |         val current = getAuthKey() ?: return null | ||||||
|  |         return InAppAuthAPI.LoginData(username = current.user, current.pass) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |         Authorize app to connect to API, using username/password. | ||||||
|  |         Required to run at startup. | ||||||
|  |         Returns OAuth entity with valid access token. | ||||||
|  |     */ | ||||||
|  |     override suspend fun initialize() { | ||||||
|  |         currentSession = getAuthKey() ?: return // just in case the following fails | ||||||
|  |         initLogin(currentSession?.user ?: return, currentSession?.pass ?: return) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun logOut() { | ||||||
|  |         setAuthKey(null) | ||||||
|  |         removeAccountKeys() | ||||||
|  |         currentSession = getAuthKey() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private suspend fun initLogin(username: String, password: String): Boolean { | ||||||
|  |         Log.i(TAG, "DATA = [$username] [$password]") | ||||||
|  |         val response = app.post( | ||||||
|  |             url = "$host/login", | ||||||
|  |             headers = mapOf( | ||||||
|  |                 "Api-Key" to apiKey, | ||||||
|  |                 "Content-Type" to "application/json" | ||||||
|  |             ), | ||||||
|  |             data = mapOf( | ||||||
|  |                 "username" to username, | ||||||
|  |                 "password" to password | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         Log.i(TAG, "Responsecode = ${response.code}") | ||||||
|  |         Log.i(TAG, "Result => ${response.text}") | ||||||
|  | 
 | ||||||
|  |         if (response.isSuccessful) { | ||||||
|  |             AppUtils.tryParseJson<OAuthToken>(response.text)?.let { token -> | ||||||
|  |                 setAuthKey( | ||||||
|  |                     SubtitleOAuthEntity( | ||||||
|  |                         user = username, | ||||||
|  |                         pass = password, | ||||||
|  |                         access_token = token.token ?: run { | ||||||
|  |                             return false | ||||||
|  |                         }) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { | ||||||
|  |         val username = data.username ?: throw ErrorLoadingException("Requires Username") | ||||||
|  |         val password = data.password ?: throw ErrorLoadingException("Requires Password") | ||||||
|  |         switchToNewAccount() | ||||||
|  |         try { | ||||||
|  |             if (initLogin(username, password)) { | ||||||
|  |                 registerAccount() | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             logError(e) | ||||||
|  |             switchToOldAccount() | ||||||
|  |         } | ||||||
|  |         switchToOldAccount() | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |        Fetch subtitles using token authenticated on previous method (see authorize). | ||||||
|  |        Returns list of Subtitles which user can select to download (see load). | ||||||
|  |     */ | ||||||
|  |     override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? { | ||||||
|  |         throwIfCantDoRequest() | ||||||
|  |         val imdbId = query.imdb ?: 0 | ||||||
|  |         val queryText = query.query.replace(" ", "+") | ||||||
|  |         val epNum = query.epNumber ?: 0 | ||||||
|  |         val seasonNum = query.seasonNumber ?: 0 | ||||||
|  |         val yearNum = query.year ?: 0 | ||||||
|  |         val epQuery = if (epNum > 0) "&episode_number=$epNum" else "" | ||||||
|  |         val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else "" | ||||||
|  |         val yearQuery = if (yearNum > 0) "&year=$yearNum" else "" | ||||||
|  | 
 | ||||||
|  |         val searchQueryUrl = when (imdbId > 0) { | ||||||
|  |             //Use imdb_id to search if its valid | ||||||
|  |             true -> "$host/subtitles?imdb_id=$imdbId&languages=${query.lang}$yearQuery$epQuery$seasonQuery" | ||||||
|  |             false -> "$host/subtitles?query=$queryText&languages=${query.lang}$yearQuery$epQuery$seasonQuery" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val req = app.get( | ||||||
|  |             url = searchQueryUrl, | ||||||
|  |             headers = mapOf( | ||||||
|  |                 Pair("Api-Key", apiKey), | ||||||
|  |                 Pair("Content-Type", "application/json") | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         Log.i(TAG, "Search Req => ${req.text}") | ||||||
|  |         if (!req.isSuccessful) { | ||||||
|  |             if (req.code == 429) | ||||||
|  |                 throwGotTooManyRequests() | ||||||
|  |             return null | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>() | ||||||
|  | 
 | ||||||
|  |         AppUtils.tryParseJson<Results>(req.text)?.let { | ||||||
|  |             it.data?.forEach { item -> | ||||||
|  |                 val attr = item.attributes ?: return@forEach | ||||||
|  |                 val featureDetails = attr.featDetails | ||||||
|  |                 //Use any valid name/title in hierarchy | ||||||
|  |                 val name = featureDetails?.movieName ?: featureDetails?.title | ||||||
|  |                 ?: featureDetails?.parentTitle ?: attr.release ?: "" | ||||||
|  |                 val lang = attr.language ?: "" | ||||||
|  |                 val resEpNum = featureDetails?.episodeNumber ?: query.epNumber | ||||||
|  |                 val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber | ||||||
|  |                 val year = featureDetails?.year ?: query.year | ||||||
|  |                 val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie | ||||||
|  |                 //Log.i(TAG, "Result id/name => ${item.id} / $name") | ||||||
|  |                 item.attributes?.files?.forEach { file -> | ||||||
|  |                     val resultData = file.fileId?.toString() ?: "" | ||||||
|  |                     //Log.i(TAG, "Result file => ${file.fileId} / ${file.fileName}") | ||||||
|  |                     results.add( | ||||||
|  |                         AbstractSubtitleEntities.SubtitleEntity( | ||||||
|  |                             idPrefix = this.idPrefix, | ||||||
|  |                             name = name, | ||||||
|  |                             lang = lang, | ||||||
|  |                             data = resultData, | ||||||
|  |                             type = type, | ||||||
|  |                             epNumber = resEpNum, | ||||||
|  |                             seasonNumber = resSeasonNum, | ||||||
|  |                             year = year | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return results | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |         Process data returned from search. | ||||||
|  |         Returns string url for the subtitle file. | ||||||
|  |     */ | ||||||
|  |     override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? { | ||||||
|  |         throwIfCantDoRequest() | ||||||
|  | 
 | ||||||
|  |         val req = app.post( | ||||||
|  |             url = "$host/download", | ||||||
|  |             headers = mapOf( | ||||||
|  |                 Pair( | ||||||
|  |                     "Authorization", | ||||||
|  |                     "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" | ||||||
|  |                 ), | ||||||
|  |                 Pair("Api-Key", apiKey), | ||||||
|  |                 Pair("Content-Type", "application/json"), | ||||||
|  |                 Pair("Accept", "*/*") | ||||||
|  |             ), | ||||||
|  |             data = mapOf( | ||||||
|  |                 Pair("file_id", data.data) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         Log.i(TAG, "Request result  => (${req.code}) ${req.text}") | ||||||
|  |         //Log.i(TAG, "Request headers => ${req.headers}") | ||||||
|  |         if (req.isSuccessful) { | ||||||
|  |             AppUtils.tryParseJson<ResultDownloadLink>(req.text)?.let { | ||||||
|  |                 val link = it.link ?: "" | ||||||
|  |                 Log.i(TAG, "Request load link => $link") | ||||||
|  |                 return link | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             if (req.code == 429) | ||||||
|  |                 throwGotTooManyRequests() | ||||||
|  |         } | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     data class SubtitleOAuthEntity( | ||||||
|  |         var user: String, | ||||||
|  |         var pass: String, | ||||||
|  |         var access_token: String, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     data class OAuthToken( | ||||||
|  |         @JsonProperty("token") var token: String? = null, | ||||||
|  |         @JsonProperty("status") var status: Int? = null | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     data class Results( | ||||||
|  |         @JsonProperty("data") var data: List<ResultData>? = listOf() | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     data class ResultData( | ||||||
|  |         @JsonProperty("id") var id: String? = null, | ||||||
|  |         @JsonProperty("type") var type: String? = null, | ||||||
|  |         @JsonProperty("attributes") var attributes: ResultAttributes? = ResultAttributes() | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     data class ResultAttributes( | ||||||
|  |         @JsonProperty("subtitle_id") var subtitleId: String? = null, | ||||||
|  |         @JsonProperty("language") var language: String? = null, | ||||||
|  |         @JsonProperty("release") var release: String? = null, | ||||||
|  |         @JsonProperty("url") var url: String? = null, | ||||||
|  |         @JsonProperty("files") var files: List<ResultFiles>? = listOf(), | ||||||
|  |         @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails() | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     data class ResultFiles( | ||||||
|  |         @JsonProperty("file_id") var fileId: Int? = null, | ||||||
|  |         @JsonProperty("file_name") var fileName: String? = null | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     data class ResultDownloadLink( | ||||||
|  |         @JsonProperty("link") var link: String? = null, | ||||||
|  |         @JsonProperty("file_name") var fileName: String? = null, | ||||||
|  |         @JsonProperty("requests") var requests: Int? = null, | ||||||
|  |         @JsonProperty("remaining") var remaining: Int? = null, | ||||||
|  |         @JsonProperty("message") var message: String? = null, | ||||||
|  |         @JsonProperty("reset_time") var resetTime: String? = null, | ||||||
|  |         @JsonProperty("reset_time_utc") var resetTimeUtc: String? = null | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     data class ResultFeatureDetails( | ||||||
|  |         @JsonProperty("year") var year: Int? = null, | ||||||
|  |         @JsonProperty("title") var title: String? = null, | ||||||
|  |         @JsonProperty("movie_name") var movieName: String? = null, | ||||||
|  |         @JsonProperty("imdb_id") var imdbId: Int? = null, | ||||||
|  |         @JsonProperty("tmdb_id") var tmdbId: Int? = null, | ||||||
|  |         @JsonProperty("season_number") var seasonNumber: Int? = null, | ||||||
|  |         @JsonProperty("episode_number") var episodeNumber: Int? = null, | ||||||
|  |         @JsonProperty("parent_imdb_id") var parentImdbId: Int? = null, | ||||||
|  |         @JsonProperty("parent_title") var parentTitle: String? = null, | ||||||
|  |         @JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null, | ||||||
|  |         @JsonProperty("parent_feature_id") var parentFeatureId: Int? = null | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | @ -33,7 +33,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
| import com.lagradost.cloudstream3.mvvm.Resource | import com.lagradost.cloudstream3.mvvm.Resource | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.mvvm.observe | import com.lagradost.cloudstream3.mvvm.observe | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis | ||||||
| import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi | import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi | ||||||
| import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi | import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi | ||||||
| import com.lagradost.cloudstream3.ui.AutofitRecyclerView | import com.lagradost.cloudstream3.ui.AutofitRecyclerView | ||||||
|  | @ -882,7 +882,7 @@ class HomeFragment : Fragment() { | ||||||
|                 home_change_api_loading?.isVisible = false |                 home_change_api_loading?.isVisible = false | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             for (syncApi in OAuth2API.OAuth2Apis) { |             for (syncApi in OAuth2Apis) { | ||||||
|                 val login = syncApi.loginInfo() |                 val login = syncApi.loginInfo() | ||||||
|                 val pic = login?.profilePicture |                 val pic = login?.profilePicture | ||||||
|                 if (home_profile_picture?.setImage( |                 if (home_profile_picture?.setImage( | ||||||
|  |  | ||||||
|  | @ -815,10 +815,6 @@ class CS3IPlayer : IPlayer { | ||||||
|                         null |                         null | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 SubtitleOrigin.OPEN_SUBTITLES -> { |  | ||||||
|                     // TODO |  | ||||||
|                     throw NotImplementedError() |  | ||||||
|                 } |  | ||||||
|                 SubtitleOrigin.EMBEDDED_IN_VIDEO -> { |                 SubtitleOrigin.EMBEDDED_IN_VIDEO -> { | ||||||
|                     if (offlineSourceFactory != null) { |                     if (offlineSourceFactory != null) { | ||||||
|                         activeSubtitles.add(sub) |                         activeSubtitles.add(sub) | ||||||
|  |  | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
| package com.lagradost.cloudstream3.ui.player | package com.lagradost.cloudstream3.ui.player | ||||||
| 
 | 
 | ||||||
| import android.annotation.SuppressLint | import android.annotation.SuppressLint | ||||||
|  | import android.app.Dialog | ||||||
|  | import android.content.Context | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
|  | import android.content.res.ColorStateList | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.util.Log | import android.util.Log | ||||||
| import android.view.LayoutInflater | import android.view.LayoutInflater | ||||||
|  | @ -23,6 +26,8 @@ import com.lagradost.cloudstream3.mvvm.Resource | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.mvvm.normalSafeApiCall | import com.lagradost.cloudstream3.mvvm.normalSafeApiCall | ||||||
| import com.lagradost.cloudstream3.mvvm.observe | import com.lagradost.cloudstream3.mvvm.observe | ||||||
|  | import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders | ||||||
| import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding | import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding | ||||||
| import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType | import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType | ||||||
| import com.lagradost.cloudstream3.ui.result.ResultEpisode | import com.lagradost.cloudstream3.ui.result.ResultEpisode | ||||||
|  | @ -30,12 +35,20 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment | ||||||
| import com.lagradost.cloudstream3.ui.result.SyncViewModel | import com.lagradost.cloudstream3.ui.result.SyncViewModel | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | ||||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment | import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment | ||||||
|  | import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 | ||||||
| import com.lagradost.cloudstream3.utils.* | import com.lagradost.cloudstream3.utils.* | ||||||
| import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||||
|  | import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread | ||||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog | import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog | ||||||
|  | import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage | ||||||
|  | import com.lagradost.cloudstream3.utils.SubtitleHelper.languages | ||||||
|  | import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI | import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage | import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage | ||||||
|  | import kotlinx.android.synthetic.main.dialog_online_subtitles.* | ||||||
|  | import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt | ||||||
|  | import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt | ||||||
| import kotlinx.android.synthetic.main.fragment_player.* | import kotlinx.android.synthetic.main.fragment_player.* | ||||||
| import kotlinx.android.synthetic.main.player_custom_layout.* | import kotlinx.android.synthetic.main.player_custom_layout.* | ||||||
| import kotlinx.android.synthetic.main.player_select_source_and_subs.* | import kotlinx.android.synthetic.main.player_select_source_and_subs.* | ||||||
|  | @ -54,6 +67,11 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private val subsProviders | ||||||
|  |         get() = subtitleProviders.filter { !it.requiresLogin || it.loginInfo() != null } | ||||||
|  |     private val subsProvidersIsActive | ||||||
|  |         get() = subsProviders.isNotEmpty() | ||||||
|  | 
 | ||||||
|     private var titleRez = 3 |     private var titleRez = 3 | ||||||
|     private var limitTitle = 0 |     private var limitTitle = 0 | ||||||
| 
 | 
 | ||||||
|  | @ -163,6 +181,174 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     data class TempMetaData( | ||||||
|  |         var episode: Int? = null, | ||||||
|  |         var season: Int? = null, | ||||||
|  |         var name: String? = null, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     private fun getMetaData(): TempMetaData { | ||||||
|  |         val meta = TempMetaData() | ||||||
|  | 
 | ||||||
|  |         when (val newMeta = currentMeta) { | ||||||
|  |             is ResultEpisode -> { | ||||||
|  |                 if (!newMeta.tvType.isMovieType()) { | ||||||
|  |                     meta.episode = newMeta.episode | ||||||
|  |                     meta.season = newMeta.season | ||||||
|  |                 } | ||||||
|  |                 meta.name = newMeta.headerName | ||||||
|  |             } | ||||||
|  |             is ExtractorUri -> { | ||||||
|  |                 if (newMeta.tvType?.isMovieType() == false) { | ||||||
|  |                     meta.episode = newMeta.episode | ||||||
|  |                     meta.season = newMeta.season | ||||||
|  |                 } | ||||||
|  |                 meta.name = newMeta.headerName | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return meta | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun openOnlineSubPicker( | ||||||
|  |         context: Context, | ||||||
|  |         imdbId: Long?, | ||||||
|  |         dismissCallback: (() -> Unit) | ||||||
|  |     ) { | ||||||
|  |         val providers = subsProviders | ||||||
|  | 
 | ||||||
|  |         val dialog = Dialog(context, R.style.AlertDialogCustomBlack) | ||||||
|  |         dialog.setContentView(R.layout.dialog_online_subtitles) | ||||||
|  | 
 | ||||||
|  |         val arrayAdapter = | ||||||
|  |             ArrayAdapter<String>(dialog.context, R.layout.sort_bottom_single_choice) | ||||||
|  | 
 | ||||||
|  |         dialog.show() | ||||||
|  | 
 | ||||||
|  |         dialog.cancel_btt.setOnClickListener { | ||||||
|  |             dialog.dismissSafe() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         dialog.subtitle_adapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE | ||||||
|  |         dialog.subtitle_adapter.adapter = arrayAdapter | ||||||
|  |         val adapter = dialog.subtitle_adapter.adapter as? ArrayAdapter<String> | ||||||
|  | 
 | ||||||
|  |         var currentSubtitles: List<AbstractSubtitleEntities.SubtitleEntity> = emptyList() | ||||||
|  |         var currentSubtitle: AbstractSubtitleEntities.SubtitleEntity? = null | ||||||
|  | 
 | ||||||
|  |         dialog.subtitle_adapter.setOnItemClickListener { _, _, position, _ -> | ||||||
|  |             currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         var currentLanguageTwoLetters: String = getAutoSelectLanguageISO639_1() | ||||||
|  | 
 | ||||||
|  |         fun getName(entry: AbstractSubtitleEntities.SubtitleEntity): String { | ||||||
|  |             return if (entry.lang.isBlank()) { | ||||||
|  |                 entry.name | ||||||
|  |             } else { | ||||||
|  |                 val language = fromTwoLettersToLanguage(entry.lang.trim()) ?: entry.lang | ||||||
|  |                 return "$language ${entry.name}" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) { | ||||||
|  |             currentSubtitles = list | ||||||
|  |             adapter?.clear() | ||||||
|  |             adapter?.addAll(currentSubtitles.map { getName(it) }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val currentTempMeta = getMetaData() | ||||||
|  |         // bruh idk why it is not correct | ||||||
|  |         val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) | ||||||
|  |         dialog.search_loading_bar.progressTintList = color | ||||||
|  |         dialog.search_loading_bar.indeterminateTintList = color | ||||||
|  | 
 | ||||||
|  |         dialog.subtitles_search.setOnQueryTextListener(object : | ||||||
|  |             androidx.appcompat.widget.SearchView.OnQueryTextListener { | ||||||
|  |             override fun onQueryTextSubmit(query: String?): Boolean { | ||||||
|  |                 dialog.search_loading_bar?.show() | ||||||
|  |                 ioSafe { | ||||||
|  |                     val search = AbstractSubtitleEntities.SubtitleSearch( | ||||||
|  |                         query = query ?: return@ioSafe, | ||||||
|  |                         imdb = imdbId, | ||||||
|  |                         epNumber = currentTempMeta.episode, | ||||||
|  |                         seasonNumber = currentTempMeta.season, | ||||||
|  |                         lang = currentLanguageTwoLetters.ifBlank { null } | ||||||
|  |                     ) | ||||||
|  |                     val results = providers.apmap { | ||||||
|  |                         try { | ||||||
|  |                             it.search(search) | ||||||
|  |                         } catch (e: Exception) { | ||||||
|  |                             null | ||||||
|  |                         } | ||||||
|  |                     }.filterNotNull() | ||||||
|  |                     val max = results.map { it.size }.maxOrNull() ?: return@ioSafe | ||||||
|  | 
 | ||||||
|  |                     // very ugly | ||||||
|  |                     val items = ArrayList<AbstractSubtitleEntities.SubtitleEntity>() | ||||||
|  |                     val arrays = results.size | ||||||
|  |                     for (index in 0 until max) { | ||||||
|  |                         for (i in 0 until arrays) { | ||||||
|  |                             items.add(results[i].getOrNull(index) ?: continue) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     // ugly ik | ||||||
|  |                     activity?.runOnUiThread { | ||||||
|  |                         setSubtitlesList(items) | ||||||
|  |                         dialog.search_loading_bar?.hide() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             override fun onQueryTextChange(newText: String?): Boolean { | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         dialog.search_filter.setOnClickListener { | ||||||
|  |             val lang639_1 = languages.map { it.ISO_639_1 } | ||||||
|  |             activity?.showDialog( | ||||||
|  |                 languages.map { it.languageName }, | ||||||
|  |                 lang639_1.indexOf(currentLanguageTwoLetters), | ||||||
|  |                 context.getString(R.string.subs_subtitle_languages), | ||||||
|  |                 true, | ||||||
|  |                 { } | ||||||
|  |             ) { index -> | ||||||
|  |                 currentLanguageTwoLetters = lang639_1[index] | ||||||
|  |                 dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         dialog.apply_btt.setOnClickListener { | ||||||
|  |             currentSubtitle?.let { currentSubtitle -> | ||||||
|  |                 providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api -> | ||||||
|  |                     ioSafe { | ||||||
|  |                         val url = api.load(currentSubtitle) ?: return@ioSafe | ||||||
|  |                         val subtitle = SubtitleData( | ||||||
|  |                             name = getName(currentSubtitle), | ||||||
|  |                             url = url, | ||||||
|  |                             origin = SubtitleOrigin.URL, | ||||||
|  |                             mimeType = url.toSubtitleMimeType() | ||||||
|  |                         ) | ||||||
|  |                         runOnMainThread { | ||||||
|  |                             addAndSelectSubtitles(subtitle) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             dialog.dismissSafe() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         dialog.setOnDismissListener { | ||||||
|  |             dismissCallback.invoke() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         dialog.show() | ||||||
|  |         dialog.subtitles_search.setQuery(currentTempMeta.name, true) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private fun openSubPicker() { |     private fun openSubPicker() { | ||||||
|         try { |         try { | ||||||
|             subsPathPicker.launch( |             subsPathPicker.launch( | ||||||
|  | @ -183,6 +369,27 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private fun addAndSelectSubtitles(subtitleData: SubtitleData) { | ||||||
|  |         val ctx = context ?: return | ||||||
|  |         setSubtitles(subtitleData) | ||||||
|  | 
 | ||||||
|  |         // this is used instead of observe, because observe is too slow | ||||||
|  |         val subs = currentSubs.toMutableSet() | ||||||
|  |         subs.add(subtitleData) | ||||||
|  |         player.setActiveSubtitles(subs) | ||||||
|  |         player.reloadPlayer(ctx) | ||||||
|  | 
 | ||||||
|  |         viewModel.addSubtitles(setOf(subtitleData)) | ||||||
|  | 
 | ||||||
|  |         selectSourceDialog?.dismissSafe() | ||||||
|  | 
 | ||||||
|  |         showToast( | ||||||
|  |             activity, | ||||||
|  |             String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name), | ||||||
|  |             Toast.LENGTH_LONG | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // Open file picker |     // Open file picker | ||||||
|     private val subsPathPicker = |     private val subsPathPicker = | ||||||
|         registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> |         registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> | ||||||
|  | @ -208,23 +415,7 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|                     name.toSubtitleMimeType() |                     name.toSubtitleMimeType() | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|                 setSubtitles(subtitleData) |                 addAndSelectSubtitles(subtitleData) | ||||||
| 
 |  | ||||||
|                 // this is used instead of observe, because observe is too slow |  | ||||||
|                 val subs = currentSubs.toMutableSet() |  | ||||||
|                 subs.add(subtitleData) |  | ||||||
|                 player.setActiveSubtitles(subs) |  | ||||||
|                 player.reloadPlayer(ctx) |  | ||||||
| 
 |  | ||||||
|                 viewModel.addSubtitles(setOf(subtitleData)) |  | ||||||
| 
 |  | ||||||
|                 selectSourceDialog?.dismissSafe() |  | ||||||
| 
 |  | ||||||
|                 showToast( |  | ||||||
|                     activity, |  | ||||||
|                     String.format(ctx.getString(R.string.player_loaded_subtitles), name), |  | ||||||
|                     Toast.LENGTH_LONG |  | ||||||
|                 ) |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -232,6 +423,7 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|     override fun showMirrorsDialogue() { |     override fun showMirrorsDialogue() { | ||||||
|         try { |         try { | ||||||
|             currentSelectedSubtitles = player.getCurrentPreferredSubtitle() |             currentSelectedSubtitles = player.getCurrentPreferredSubtitle() | ||||||
|  |             println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs") | ||||||
|             context?.let { ctx -> |             context?.let { ctx -> | ||||||
|                 val isPlaying = player.getIsPlaying() |                 val isPlaying = player.getIsPlaying() | ||||||
|                 player.handleEvent(CSPlayerEvent.Pause) |                 player.handleEvent(CSPlayerEvent.Pause) | ||||||
|  | @ -246,13 +438,43 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|                 val providerList = sourceDialog.sort_providers |                 val providerList = sourceDialog.sort_providers | ||||||
|                 val subtitleList = sourceDialog.sort_subtitles |                 val subtitleList = sourceDialog.sort_subtitles | ||||||
| 
 | 
 | ||||||
|                 val footer: TextView = |                 val loadFromFileFooter: TextView = | ||||||
|                     layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView |                     layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView | ||||||
|                 footer.text = ctx.getString(R.string.player_load_subtitles) | 
 | ||||||
|                 footer.setOnClickListener { |                 loadFromFileFooter.text = ctx.getString(R.string.player_load_subtitles) | ||||||
|  |                 loadFromFileFooter.setOnClickListener { | ||||||
|                     openSubPicker() |                     openSubPicker() | ||||||
|                 } |                 } | ||||||
|                 subtitleList.addFooterView(footer) |                 subtitleList.addFooterView(loadFromFileFooter) | ||||||
|  | 
 | ||||||
|  |                 var shouldDismiss = true | ||||||
|  | 
 | ||||||
|  |                 fun dismiss() { | ||||||
|  |                     if (isPlaying) { | ||||||
|  |                         player.handleEvent(CSPlayerEvent.Play) | ||||||
|  |                     } | ||||||
|  |                     activity?.hideSystemUI() | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (subsProvidersIsActive) { | ||||||
|  |                     val loadFromOpenSubsFooter: TextView = | ||||||
|  |                         layoutInflater.inflate( | ||||||
|  |                             R.layout.sort_bottom_footer_add_choice, | ||||||
|  |                             null | ||||||
|  |                         ) as TextView | ||||||
|  | 
 | ||||||
|  |                     loadFromOpenSubsFooter.text = | ||||||
|  |                         ctx.getString(R.string.player_load_subtitles_online) | ||||||
|  | 
 | ||||||
|  |                     loadFromOpenSubsFooter.setOnClickListener { | ||||||
|  |                         shouldDismiss = false | ||||||
|  |                         sourceDialog.dismissSafe(activity) | ||||||
|  |                         openOnlineSubPicker(it.context, null) { | ||||||
|  |                             dismiss() | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     subtitleList.addFooterView(loadFromOpenSubsFooter) | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 var sourceIndex = 0 |                 var sourceIndex = 0 | ||||||
|                 var startSource = 0 |                 var startSource = 0 | ||||||
|  | @ -283,15 +505,6 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 var shouldDismiss = true |  | ||||||
| 
 |  | ||||||
|                 fun dismiss() { |  | ||||||
|                     if (isPlaying) { |  | ||||||
|                         player.handleEvent(CSPlayerEvent.Play) |  | ||||||
|                     } |  | ||||||
|                     activity?.hideSystemUI() |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 sourceDialog.setOnDismissListener { |                 sourceDialog.setOnDismissListener { | ||||||
|                     if (shouldDismiss) dismiss() |                     if (shouldDismiss) dismiss() | ||||||
|                     selectSourceDialog = null |                     selectSourceDialog = null | ||||||
|  | @ -598,18 +811,15 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @SuppressLint("SetTextI18n") |     private fun getPlayerVideoTitle(): String { | ||||||
|     fun setTitle() { |  | ||||||
|         var headerName: String? = null |         var headerName: String? = null | ||||||
|         var subName: String? = null |         var subName: String? = null | ||||||
|         var episode: Int? = null |         var episode: Int? = null | ||||||
|         var season: Int? = null |         var season: Int? = null | ||||||
|         var tvType: TvType? = null |         var tvType: TvType? = null | ||||||
| 
 | 
 | ||||||
|         var isFiller: Boolean? = null |  | ||||||
|         when (val meta = currentMeta) { |         when (val meta = currentMeta) { | ||||||
|             is ResultEpisode -> { |             is ResultEpisode -> { | ||||||
|                 isFiller = meta.isFiller |  | ||||||
|                 headerName = meta.headerName |                 headerName = meta.headerName | ||||||
|                 subName = meta.name |                 subName = meta.name | ||||||
|                 episode = meta.episode |                 episode = meta.episode | ||||||
|  | @ -626,39 +836,40 @@ class GeneratorPlayer : FullScreenPlayer() { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         //Generate video title |         //Generate video title | ||||||
|         context?.let { ctx -> |         val playerVideoTitle = if (headerName != null) { | ||||||
|             var playerVideoTitle = if (headerName != null) { |             (headerName + | ||||||
|                 (headerName + |                     if (tvType.isEpisodeBased() && episode != null) | ||||||
|                         if (tvType.isEpisodeBased() && episode != null) |                         if (season == null) | ||||||
|                             if (season == null) |                             " - ${getString(R.string.episode)} $episode" | ||||||
|                                 " - ${ctx.getString(R.string.episode)} $episode" |                         else | ||||||
|                             else |                             " \"${getString(R.string.season_short)}${season}:${getString(R.string.episode_short)}${episode}\"" | ||||||
|                                 " \"${ctx.getString(R.string.season_short)}${season}:${ |                     else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName" | ||||||
|                                     ctx.getString( |         } else { | ||||||
|                                         R.string.episode_short |             "" | ||||||
|                                     ) |  | ||||||
|                                 }${episode}\"" |  | ||||||
|                         else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName" |  | ||||||
|             } else { |  | ||||||
|                 "" |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             //Hide title, if set in setting |  | ||||||
|             if (limitTitle < 0) { |  | ||||||
|                 player_video_title?.visibility = View.GONE |  | ||||||
|             } 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 |  | ||||||
|         } |         } | ||||||
|  |         return playerVideoTitle | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |     @SuppressLint("SetTextI18n") | ||||||
|  |     fun setTitle() { | ||||||
|  |         var playerVideoTitle = getPlayerVideoTitle() | ||||||
|  | 
 | ||||||
|  |         //Hide title, if set in setting | ||||||
|  |         if (limitTitle < 0) { | ||||||
|  |             player_video_title?.visibility = View.GONE | ||||||
|  |         } 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) + "..." | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller | ||||||
|  | 
 | ||||||
|  |         player_episode_filler_holder?.isVisible = isFiller ?: false | ||||||
|  |         player_video_title?.text = playerVideoTitle | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @SuppressLint("SetTextI18n") |     @SuppressLint("SetTextI18n") | ||||||
|  |  | ||||||
|  | @ -1,13 +1,10 @@ | ||||||
| package com.lagradost.cloudstream3.ui.player | package com.lagradost.cloudstream3.ui.player | ||||||
| 
 | 
 | ||||||
| import android.content.Context |  | ||||||
| import android.net.Uri |  | ||||||
| import android.util.TypedValue | import android.util.TypedValue | ||||||
| import android.view.ViewGroup | import android.view.ViewGroup | ||||||
| import android.widget.FrameLayout | import android.widget.FrameLayout | ||||||
| import com.google.android.exoplayer2.ui.SubtitleView | import com.google.android.exoplayer2.ui.SubtitleView | ||||||
| import com.google.android.exoplayer2.util.MimeTypes | import com.google.android.exoplayer2.util.MimeTypes | ||||||
| import com.hippo.unifile.UniFile |  | ||||||
| import com.lagradost.cloudstream3.SubtitleFile | import com.lagradost.cloudstream3.SubtitleFile | ||||||
| import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat | import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat | ||||||
| import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveCaptions | import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveCaptions | ||||||
|  | @ -24,7 +21,6 @@ enum class SubtitleStatus { | ||||||
| enum class SubtitleOrigin { | enum class SubtitleOrigin { | ||||||
|     URL, |     URL, | ||||||
|     DOWNLOADED_FILE, |     DOWNLOADED_FILE, | ||||||
|     OPEN_SUBTITLES, |  | ||||||
|     EMBEDDED_IN_VIDEO |     EMBEDDED_IN_VIDEO | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -68,28 +64,6 @@ class PlayerSubtitleHelper { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private fun getSubtitleMimeType(context: Context, url: String, origin: SubtitleOrigin): String { |  | ||||||
|             return when (origin) { |  | ||||||
|                 // The url can look like .../document/4294 when the name is EnglishSDH.srt |  | ||||||
|                 SubtitleOrigin.DOWNLOADED_FILE -> { |  | ||||||
|                     UniFile.fromUri( |  | ||||||
|                         context, |  | ||||||
|                         Uri.parse(url) |  | ||||||
|                     ).name?.toSubtitleMimeType() ?: MimeTypes.APPLICATION_SUBRIP |  | ||||||
|                 } |  | ||||||
|                 SubtitleOrigin.URL -> { |  | ||||||
|                     return url.toSubtitleMimeType() |  | ||||||
|                 } |  | ||||||
|                 SubtitleOrigin.OPEN_SUBTITLES -> { |  | ||||||
|                     // TODO |  | ||||||
|                     throw NotImplementedError() |  | ||||||
|                 } |  | ||||||
|                 SubtitleOrigin.EMBEDDED_IN_VIDEO -> { |  | ||||||
|                     throw NotImplementedError() |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         fun getSubtitleData(subtitleFile: SubtitleFile): SubtitleData { |         fun getSubtitleData(subtitleFile: SubtitleFile): SubtitleData { | ||||||
|             return SubtitleData( |             return SubtitleData( | ||||||
|                 name = subtitleFile.lang, |                 name = subtitleFile.lang, | ||||||
|  |  | ||||||
|  | @ -24,7 +24,6 @@ import android.widget.* | ||||||
| import androidx.annotation.StringRes | import androidx.annotation.StringRes | ||||||
| import androidx.appcompat.app.AlertDialog | import androidx.appcompat.app.AlertDialog | ||||||
| import androidx.core.content.FileProvider | import androidx.core.content.FileProvider | ||||||
| import androidx.core.graphics.drawable.toBitmap |  | ||||||
| import androidx.core.view.isGone | import androidx.core.view.isGone | ||||||
| import androidx.core.view.isVisible | import androidx.core.view.isVisible | ||||||
| import androidx.core.widget.NestedScrollView | import androidx.core.widget.NestedScrollView | ||||||
|  | @ -41,7 +40,6 @@ import com.google.android.gms.cast.framework.CastContext | ||||||
| import com.google.android.gms.cast.framework.CastState | import com.google.android.gms.cast.framework.CastState | ||||||
| import com.google.android.material.button.MaterialButton | import com.google.android.material.button.MaterialButton | ||||||
| import com.lagradost.cloudstream3.* | import com.lagradost.cloudstream3.* | ||||||
| import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings |  | ||||||
| import com.lagradost.cloudstream3.APIHolder.getApiFromName | import com.lagradost.cloudstream3.APIHolder.getApiFromName | ||||||
| import com.lagradost.cloudstream3.APIHolder.getId | import com.lagradost.cloudstream3.APIHolder.getId | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
|  | @ -1353,7 +1351,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | ||||||
|             val newList = list.filter { it.isSynced && it.hasAccount } |             val newList = list.filter { it.isSynced && it.hasAccount } | ||||||
| 
 | 
 | ||||||
|             result_mini_sync?.isVisible = newList.isNotEmpty() |             result_mini_sync?.isVisible = newList.isNotEmpty() | ||||||
|             (result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.map { it.icon }) |             (result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon }) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         observe(syncModel.syncIds) { |         observe(syncModel.syncIds) { | ||||||
|  |  | ||||||
|  | @ -7,9 +7,9 @@ import androidx.lifecycle.ViewModel | ||||||
| import androidx.lifecycle.viewModelScope | import androidx.lifecycle.viewModelScope | ||||||
| import com.lagradost.cloudstream3.apmap | import com.lagradost.cloudstream3.apmap | ||||||
| import com.lagradost.cloudstream3.mvvm.Resource | import com.lagradost.cloudstream3.mvvm.Resource | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi | ||||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||||
| import com.lagradost.cloudstream3.utils.SyncUtil | import com.lagradost.cloudstream3.utils.SyncUtil | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
|  | @ -20,7 +20,7 @@ data class CurrentSynced( | ||||||
|     val idPrefix: String, |     val idPrefix: String, | ||||||
|     val isSynced: Boolean, |     val isSynced: Boolean, | ||||||
|     val hasAccount: Boolean, |     val hasAccount: Boolean, | ||||||
|     val icon: Int, |     val icon: Int?, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| class SyncViewModel : ViewModel() { | class SyncViewModel : ViewModel() { | ||||||
|  |  | ||||||
|  | @ -3,10 +3,10 @@ package com.lagradost.cloudstream3.ui.search | ||||||
| import com.lagradost.cloudstream3.SearchQuality | import com.lagradost.cloudstream3.SearchQuality | ||||||
| import com.lagradost.cloudstream3.SearchResponse | import com.lagradost.cloudstream3.SearchResponse | ||||||
| import com.lagradost.cloudstream3.TvType | import com.lagradost.cloudstream3.TvType | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis | ||||||
| 
 | 
 | ||||||
| class SyncSearchViewModel { | class SyncSearchViewModel { | ||||||
|     private val repos = OAuth2API.SyncApis |     private val repos = SyncApis | ||||||
| 
 | 
 | ||||||
|     data class SyncSearchResultSearchResponse( |     data class SyncSearchResultSearchResponse( | ||||||
|         override val name: String, |         override val name: String, | ||||||
|  | @ -18,5 +18,4 @@ class SyncSearchViewModel { | ||||||
|         override var quality: SearchQuality? = null, |         override var quality: SearchQuality? = null, | ||||||
|         override var posterHeaders: Map<String, String>? = null, |         override var posterHeaders: Map<String, String>? = null, | ||||||
|     ) : SearchResponse |     ) : SearchResponse | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  | @ -8,13 +8,13 @@ import android.widget.TextView | ||||||
| import androidx.core.view.isVisible | import androidx.core.view.isVisible | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import com.lagradost.cloudstream3.R | import com.lagradost.cloudstream3.R | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.setImage | import com.lagradost.cloudstream3.utils.UIHelper.setImage | ||||||
| 
 | 
 | ||||||
| class AccountClickCallback(val action: Int, val view : View, val card: OAuth2API.LoginInfo) | class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo) | ||||||
| 
 | 
 | ||||||
| class AccountAdapter( | class AccountAdapter( | ||||||
|     val cardList: List<OAuth2API.LoginInfo>, |     val cardList: List<AuthAPI.LoginInfo>, | ||||||
|     val layout: Int = R.layout.account_single, |     val layout: Int = R.layout.account_single, | ||||||
|     private val clickCallback: (AccountClickCallback) -> Unit |     private val clickCallback: (AccountClickCallback) -> Unit | ||||||
| ) : | ) : | ||||||
|  | @ -48,15 +48,13 @@ class AccountAdapter( | ||||||
|         private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! |         private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! | ||||||
|         private val accountName: TextView = itemView.findViewById(R.id.account_name)!! |         private val accountName: TextView = itemView.findViewById(R.id.account_name)!! | ||||||
| 
 | 
 | ||||||
|         fun bind(card: OAuth2API.LoginInfo) { |         fun bind(card: AuthAPI.LoginInfo) { | ||||||
|             // just in case name is null account index will show, should never happened |             // just in case name is null account index will show, should never happened | ||||||
|             accountName.text = card.name ?: "%s %d".format(accountName.context.getString(R.string.account), card.accountIndex) |             accountName.text = card.name ?: "%s %d".format( | ||||||
|             if(card.profilePicture.isNullOrEmpty()) { |                 accountName.context.getString(R.string.account), | ||||||
|                 pfp.isVisible = false |                 card.accountIndex | ||||||
|             } else { |             ) | ||||||
|                 pfp.isVisible = true |             pfp.isVisible = pfp.setImage(card.profilePicture) | ||||||
|                 pfp.setImage(card.profilePicture) |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             itemView.setOnClickListener { |             itemView.setOnClickListener { | ||||||
|                 clickCallback.invoke(AccountClickCallback(0, itemView, card)) |                 clickCallback.invoke(AccountClickCallback(0, itemView, card)) | ||||||
|  |  | ||||||
|  | @ -1,38 +1,56 @@ | ||||||
| package com.lagradost.cloudstream3.ui.settings | package com.lagradost.cloudstream3.ui.settings | ||||||
| 
 | 
 | ||||||
| import android.content.Context | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.net.Uri | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.view.View | import android.view.View | ||||||
| import android.widget.ImageView |  | ||||||
| import android.widget.TextView | import android.widget.TextView | ||||||
|  | import androidx.annotation.UiThread | ||||||
| import androidx.appcompat.app.AlertDialog | import androidx.appcompat.app.AlertDialog | ||||||
|  | import androidx.core.view.isGone | ||||||
|  | import androidx.core.view.isVisible | ||||||
| import androidx.preference.PreferenceFragmentCompat | import androidx.preference.PreferenceFragmentCompat | ||||||
| import androidx.preference.PreferenceManager | import androidx.preference.PreferenceManager | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import com.lagradost.cloudstream3.CommonActivity.showToast | ||||||
| import com.lagradost.cloudstream3.R | import com.lagradost.cloudstream3.R | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager | import com.lagradost.cloudstream3.syncproviders.AccountManager | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.nginxApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.OAuth2API | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.beneneCount | import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.beneneCount | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref | import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar | import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar | ||||||
|  | import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.setImage | import com.lagradost.cloudstream3.utils.UIHelper.setImage | ||||||
|  | import kotlinx.android.synthetic.main.account_managment.* | ||||||
|  | import kotlinx.android.synthetic.main.account_switch.* | ||||||
|  | import kotlinx.android.synthetic.main.add_account_input.* | ||||||
| 
 | 
 | ||||||
| class SettingsAccount : PreferenceFragmentCompat() { | class SettingsAccount : PreferenceFragmentCompat() { | ||||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|         super.onViewCreated(view, savedInstanceState) |         super.onViewCreated(view, savedInstanceState) | ||||||
|         setUpToolbar(R.string.category_credits) |         setUpToolbar(R.string.category_credits) | ||||||
|     } |     } | ||||||
|     private fun showLoginInfo(api: AccountManager, info: OAuth2API.LoginInfo) { | 
 | ||||||
|  |     private fun showLoginInfo(api: AccountManager, info: AuthAPI.LoginInfo) { | ||||||
|         val builder = |         val builder = | ||||||
|             AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) |             AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) | ||||||
|                 .setView(R.layout.account_managment) |                 .setView(R.layout.account_managment) | ||||||
|         val dialog = builder.show() |         val dialog = builder.show() | ||||||
| 
 | 
 | ||||||
|         dialog.findViewById<ImageView>(R.id.account_profile_picture)?.setImage(info.profilePicture) |         dialog.account_main_profile_picture_holder?.isVisible = | ||||||
|         dialog.findViewById<TextView>(R.id.account_logout)?.setOnClickListener { |             dialog.account_main_profile_picture?.setImage(info.profilePicture) == true | ||||||
|  | 
 | ||||||
|  |         dialog.account_logout?.setOnClickListener { | ||||||
|             api.logOut() |             api.logOut() | ||||||
|             dialog.dismissSafe(activity) |             dialog.dismissSafe(activity) | ||||||
|         } |         } | ||||||
|  | @ -40,13 +58,93 @@ class SettingsAccount : PreferenceFragmentCompat() { | ||||||
|         (info.name ?: context?.getString(R.string.no_data))?.let { |         (info.name ?: context?.getString(R.string.no_data))?.let { | ||||||
|             dialog.findViewById<TextView>(R.id.account_name)?.text = it |             dialog.findViewById<TextView>(R.id.account_name)?.text = it | ||||||
|         } |         } | ||||||
|         dialog.findViewById<TextView>(R.id.account_site)?.text = api.name | 
 | ||||||
|         dialog.findViewById<TextView>(R.id.account_switch_account)?.setOnClickListener { |         dialog.account_site?.text = api.name | ||||||
|  |         dialog.account_switch_account?.setOnClickListener { | ||||||
|             dialog.dismissSafe(activity) |             dialog.dismissSafe(activity) | ||||||
|             showAccountSwitch(it.context, api) |             showAccountSwitch(it.context, api) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @UiThread | ||||||
|  |     private fun addAccount(api: AccountManager) { | ||||||
|  |         try { | ||||||
|  |             when (api) { | ||||||
|  |                 is OAuth2API -> { | ||||||
|  |                     api.authenticate() | ||||||
|  |                 } | ||||||
|  |                 is InAppAuthAPI -> { | ||||||
|  |                     val builder = | ||||||
|  |                         AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) | ||||||
|  |                             .setView(R.layout.add_account_input) | ||||||
|  |                     val dialog = builder.show() | ||||||
|  |                     dialog.login_email_input?.isVisible = api.requiresEmail | ||||||
|  |                     dialog.login_password_input?.isVisible = api.requiresPassword | ||||||
|  |                     dialog.login_server_input?.isVisible = api.requiresServer | ||||||
|  |                     dialog.login_username_input?.isVisible = api.requiresUsername | ||||||
|  |                     dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() | ||||||
|  |                     dialog.create_account?.setOnClickListener { | ||||||
|  |                         val i = Intent(Intent.ACTION_VIEW) | ||||||
|  |                         i.data = Uri.parse(api.createAccountUrl) | ||||||
|  |                         try { | ||||||
|  |                             startActivity(i) | ||||||
|  |                         } catch (e: Exception) { | ||||||
|  |                             logError(e) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     dialog.text1?.text = api.name | ||||||
|  | 
 | ||||||
|  |                     if (api.storesPasswordInPlainText) { | ||||||
|  |                         api.getLatestLoginData()?.let { data -> | ||||||
|  |                             dialog.login_email_input?.setText(data.email ?: "") | ||||||
|  |                             dialog.login_server_input?.setText(data.server ?: "") | ||||||
|  |                             dialog.login_username_input?.setText(data.username ?: "") | ||||||
|  |                             dialog.login_password_input?.setText(data.password ?: "") | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     dialog.apply_btt?.setOnClickListener { | ||||||
|  |                         val loginData = InAppAuthAPI.LoginData( | ||||||
|  |                             username = if (api.requiresUsername) dialog.login_username_input?.text?.toString() else null, | ||||||
|  |                             password = if (api.requiresPassword) dialog.login_password_input?.text?.toString() else null, | ||||||
|  |                             email = if (api.requiresEmail) dialog.login_email_input?.text?.toString() else null, | ||||||
|  |                             server = if (api.requiresServer) dialog.login_server_input?.text?.toString() else null, | ||||||
|  |                         ) | ||||||
|  |                         ioSafe { | ||||||
|  |                             val isSuccessful = try { | ||||||
|  |                                 api.login(loginData) | ||||||
|  |                             } catch (e: Exception) { | ||||||
|  |                                 logError(e) | ||||||
|  |                                 false | ||||||
|  |                             } | ||||||
|  |                             activity?.runOnUiThread { | ||||||
|  |                                 try { | ||||||
|  |                                     showToast( | ||||||
|  |                                         activity, | ||||||
|  |                                         getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( | ||||||
|  |                                             api.name | ||||||
|  |                                         ) | ||||||
|  |                                     ) | ||||||
|  |                                 } catch (e: Exception) { | ||||||
|  |                                     logError(e) // format might fail | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         dialog.dismissSafe(activity) | ||||||
|  |                     } | ||||||
|  |                     dialog.cancel_btt?.setOnClickListener { | ||||||
|  |                         dialog.dismissSafe(activity) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 else -> { | ||||||
|  |                     throw NotImplementedError("You are trying to add an account that has an unknown login method") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             logError(e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private fun showAccountSwitch(context: Context, api: AccountManager) { |     private fun showAccountSwitch(context: Context, api: AccountManager) { | ||||||
|         val accounts = api.getAccounts() ?: return |         val accounts = api.getAccounts() ?: return | ||||||
| 
 | 
 | ||||||
|  | @ -54,17 +152,14 @@ class SettingsAccount : PreferenceFragmentCompat() { | ||||||
|             AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch) |             AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch) | ||||||
|         val dialog = builder.show() |         val dialog = builder.show() | ||||||
| 
 | 
 | ||||||
|         dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener { |         dialog.account_add?.setOnClickListener { | ||||||
|             try { |             addAccount(api) | ||||||
|                 api.authenticate() |             dialog?.dismissSafe(activity) | ||||||
|             } catch (e: Exception) { |  | ||||||
|                 logError(e) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         val ogIndex = api.accountIndex |         val ogIndex = api.accountIndex | ||||||
| 
 | 
 | ||||||
|         val items = ArrayList<OAuth2API.LoginInfo>() |         val items = ArrayList<AuthAPI.LoginInfo>() | ||||||
| 
 | 
 | ||||||
|         for (index in accounts) { |         for (index in accounts) { | ||||||
|             api.accountIndex = index |             api.accountIndex = index | ||||||
|  | @ -97,33 +192,28 @@ class SettingsAccount : PreferenceFragmentCompat() { | ||||||
| 
 | 
 | ||||||
|         val syncApis = |         val syncApis = | ||||||
|             listOf( |             listOf( | ||||||
|                 Pair(R.string.mal_key, OAuth2API.malApi), Pair( |                 R.string.mal_key to malApi, | ||||||
|                     R.string.anilist_key, |                 R.string.anilist_key to aniListApi, | ||||||
|                     OAuth2API.aniListApi |                 R.string.opensubtitles_key to openSubtitlesApi, | ||||||
|                 ) |                 R.string.nginx_key to nginxApi, | ||||||
|             ) |             ) | ||||||
|  | 
 | ||||||
|         for ((key, api) in syncApis) { |         for ((key, api) in syncApis) { | ||||||
|             getPref(key)?.apply { |             getPref(key)?.apply { | ||||||
|                 title = |                 title = | ||||||
|                     getString(R.string.login_format).format(api.name, getString(R.string.account)) |                     getString(R.string.login_format).format(api.name, getString(R.string.account)) | ||||||
|                 setOnPreferenceClickListener { _ -> |                 setOnPreferenceClickListener { | ||||||
|                     val info = api.loginInfo() |                     val info = api.loginInfo() | ||||||
|                     if (info != null) { |                     if (info != null) { | ||||||
|                         showLoginInfo(api, info) |                         showLoginInfo(api, info) | ||||||
|                     } else { |                     } else { | ||||||
|                         try { |                         addAccount(api) | ||||||
|                             api.authenticate() |  | ||||||
|                         } catch (e: Exception) { |  | ||||||
|                             logError(e) |  | ||||||
|                         } |  | ||||||
|                     } |                     } | ||||||
|                     return@setOnPreferenceClickListener true |                     return@setOnPreferenceClickListener true | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         try { |         try { | ||||||
|             beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0) |             beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0) | ||||||
|             getPref(R.string.benene_count)?.let { pref -> |             getPref(R.string.benene_count)?.let { pref -> | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ import androidx.preference.PreferenceFragmentCompat | ||||||
| import androidx.preference.PreferenceManager | import androidx.preference.PreferenceManager | ||||||
| import com.lagradost.cloudstream3.R | import com.lagradost.cloudstream3.R | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers | ||||||
| import com.lagradost.cloudstream3.ui.home.HomeFragment | import com.lagradost.cloudstream3.ui.home.HomeFragment | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.navigate | import com.lagradost.cloudstream3.utils.UIHelper.navigate | ||||||
|  | @ -116,7 +116,7 @@ class SettingsFragment : Fragment() { | ||||||
| 
 | 
 | ||||||
|         val isTrueTv = context?.isTrueTvSettings() == true |         val isTrueTv = context?.isTrueTvSettings() == true | ||||||
| 
 | 
 | ||||||
|         for (syncApi in OAuth2API.OAuth2Apis) { |         for (syncApi in accountManagers) { | ||||||
|             val login = syncApi.loginInfo() |             val login = syncApi.loginInfo() | ||||||
|             val pic = login?.profilePicture ?: continue |             val pic = login?.profilePicture ?: continue | ||||||
|             if (settings_profile_pic?.setImage( |             if (settings_profile_pic?.setImage( | ||||||
|  | @ -135,7 +135,6 @@ class SettingsFragment : Fragment() { | ||||||
|             Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account), |             Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account), | ||||||
|             Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui), |             Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui), | ||||||
|             Pair(settings_lang, R.id.action_navigation_settings_to_navigation_settings_lang), |             Pair(settings_lang, R.id.action_navigation_settings_to_navigation_settings_lang), | ||||||
|             Pair(settings_nginx, R.id.action_navigation_settings_to_navigation_settings_nginx), |  | ||||||
|             Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates), |             Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates), | ||||||
|         ).forEach { (view, navigationId) -> |         ).forEach { (view, navigationId) -> | ||||||
|             view?.apply { |             view?.apply { | ||||||
|  |  | ||||||
|  | @ -116,7 +116,7 @@ class SettingsLang : PreferenceFragmentCompat() { | ||||||
|             return@setOnPreferenceClickListener true |             return@setOnPreferenceClickListener true | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref -> |         getPref(R.string.locale_key)?.setOnPreferenceClickListener { | ||||||
|             val tempLangs = languages.toMutableList() |             val tempLangs = languages.toMutableList() | ||||||
|             //if (beneneCount > 100) { |             //if (beneneCount > 100) { | ||||||
|             //    tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo")) |             //    tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo")) | ||||||
|  |  | ||||||
|  | @ -1,55 +0,0 @@ | ||||||
| package com.lagradost.cloudstream3.ui.settings |  | ||||||
| 
 |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.View |  | ||||||
| import androidx.preference.PreferenceFragmentCompat |  | ||||||
| import androidx.preference.PreferenceManager |  | ||||||
| import com.lagradost.cloudstream3.R |  | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref |  | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar |  | ||||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog |  | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard |  | ||||||
| 
 |  | ||||||
| class SettingsNginx : PreferenceFragmentCompat() { |  | ||||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |  | ||||||
|         super.onViewCreated(view, savedInstanceState) |  | ||||||
|         setUpToolbar(R.string.category_nginx) |  | ||||||
|     } |  | ||||||
|     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { |  | ||||||
|         hideKeyboard() |  | ||||||
|         setPreferencesFromResource(R.xml.settings_nginx, rootKey) |  | ||||||
|         val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) |  | ||||||
| 
 |  | ||||||
|         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.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 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -45,7 +45,6 @@ object SubtitleHelper { | ||||||
|      * @param looseCheck will use .contains in addition to .equals |      * @param looseCheck will use .contains in addition to .equals | ||||||
|      * */ |      * */ | ||||||
|     fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? { |     fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? { | ||||||
| 
 |  | ||||||
|         languages.forEach { |         languages.forEach { | ||||||
|             if (it.languageName.equals(input, ignoreCase = true) |             if (it.languageName.equals(input, ignoreCase = true) | ||||||
|                 || it.nativeName.equals(input, ignoreCase = true) |                 || it.nativeName.equals(input, ignoreCase = true) | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								app/src/main/res/drawable/nginx.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/src/main/res/drawable/nginx.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |         android:name="vector" | ||||||
|  |         android:width="24dp" | ||||||
|  |         android:height="24dp" | ||||||
|  |         android:tint="?attr/white" | ||||||
|  |         android:viewportWidth="283" | ||||||
|  |         android:viewportHeight="283"> | ||||||
|  |     <path | ||||||
|  |             android:name="path" | ||||||
|  |             android:pathData="M 253.41 62.61 L 154.22 5.34 C 150.42 3.146 146.108 1.991 141.72 1.991 C 137.332 1.991 133.02 3.146 129.22 5.34 L 30 62.61 C 26.202 64.807 23.049 67.966 20.858 71.768 C 18.668 75.57 17.516 79.882 17.52 84.27 L 17.52 198.8 C 17.516 203.188 18.668 207.5 20.858 211.302 C 23.049 215.104 26.202 218.263 30 220.46 L 129.19 277.72 C 132.99 279.914 137.302 281.069 141.69 281.069 C 146.078 281.069 150.39 279.914 154.19 277.72 L 253.38 220.46 C 257.183 218.266 260.343 215.109 262.539 211.307 C 264.735 207.505 265.891 203.191 265.89 198.8 L 265.89 84.27 C 265.894 79.882 264.742 75.57 262.552 71.768 C 260.361 67.966 257.208 64.807 253.41 62.61 Z M 203.28 185.33 Q 203.28 200.61 187.03 200.61 C 184.56 200.637 182.098 200.331 179.71 199.7 C 177.529 199.086 175.467 198.109 173.61 196.81 C 171.687 195.463 169.917 193.91 168.33 192.18 Q 165.9 189.52 163.45 186.76 L 106.86 119.16 L 106.86 187.16 Q 106.86 193.81 102.86 197.22 C 100.004 199.558 96.388 200.768 92.7 200.62 Q 86.3 200.62 82.44 197.18 Q 78.58 193.74 78.58 187.18 L 78.58 97.63 C 78.438 94.563 78.992 91.503 80.2 88.68 C 81.685 86.126 83.925 84.093 86.61 82.86 C 89.603 81.356 92.911 80.585 96.26 80.61 C 98.633 80.541 101.001 80.879 103.26 81.61 C 105.096 82.243 106.813 83.179 108.34 84.38 C 109.979 85.728 111.477 87.239 112.81 88.89 C 114.33 90.74 115.91 92.66 117.53 94.67 L 175.53 163.06 L 175.53 94.06 Q 175.53 87.34 179.24 83.97 Q 182.95 80.6 189.24 80.61 C 193.57 80.61 197 81.73 199.5 83.97 C 202 86.21 203.26 89.58 203.26 94.06 Z" | ||||||
|  |             android:fillColor="#000" | ||||||
|  |             android:strokeWidth="1" /> | ||||||
|  | </vector> | ||||||
							
								
								
									
										18
									
								
								app/src/main/res/drawable/nginx_question.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/src/main/res/drawable/nginx_question.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |         android:name="vector" | ||||||
|  |         android:tint="?attr/white" | ||||||
|  |         android:width="35dp" | ||||||
|  |         android:height="26dp" | ||||||
|  |         android:viewportWidth="379" | ||||||
|  |         android:viewportHeight="279"> | ||||||
|  |     <path | ||||||
|  |             android:name="path" | ||||||
|  |             android:pathData="M 235.89 60.62 L 136.7 3.35 C 132.9 1.156 128.588 0.001 124.2 0.001 C 119.812 0.001 115.5 1.156 111.7 3.35 L 12.48 60.62 C 8.682 62.817 5.529 65.976 3.338 69.778 C 1.148 73.58 -0.004 77.892 0 82.28 L 0 196.81 C -0.004 201.198 1.148 205.51 3.338 209.312 C 5.529 213.114 8.682 216.273 12.48 218.47 L 111.67 275.73 C 115.47 277.924 119.782 279.079 124.17 279.079 C 128.558 279.079 132.87 277.924 136.67 275.73 L 235.86 218.47 C 239.663 216.276 242.823 213.119 245.019 209.317 C 247.215 205.515 248.371 201.201 248.37 196.81 L 248.37 82.28 C 248.374 77.892 247.222 73.58 245.032 69.778 C 242.841 65.976 239.688 62.817 235.89 60.62 Z M 185.76 183.34 Q 185.76 198.62 169.51 198.62 C 167.04 198.647 164.578 198.341 162.19 197.71 C 160.009 197.096 157.947 196.119 156.09 194.82 C 154.167 193.473 152.397 191.92 150.81 190.19 Q 148.38 187.53 145.93 184.77 L 89.34 117.17 L 89.34 185.17 Q 89.34 191.82 85.34 195.23 C 82.484 197.568 78.868 198.778 75.18 198.63 Q 68.78 198.63 64.92 195.19 Q 61.06 191.75 61.06 185.19 L 61.06 95.64 C 60.918 92.573 61.472 89.513 62.68 86.69 C 64.165 84.136 66.405 82.103 69.09 80.87 C 72.083 79.366 75.391 78.595 78.74 78.62 C 81.113 78.551 83.481 78.889 85.74 79.62 C 87.576 80.253 89.293 81.189 90.82 82.39 C 92.459 83.738 93.957 85.249 95.29 86.9 C 96.81 88.75 98.39 90.67 100.01 92.68 L 158.01 161.07 L 158.01 92.07 Q 158.01 85.35 161.72 81.98 Q 165.43 78.61 171.72 78.62 C 176.05 78.62 179.48 79.74 181.98 81.98 C 184.48 84.22 185.74 87.59 185.74 92.07 Z" | ||||||
|  |             android:fillColor="#000" | ||||||
|  |             android:strokeWidth="1" /> | ||||||
|  |     <path | ||||||
|  |             android:name="path_1" | ||||||
|  |             android:pathData="M 312.84 143.37 C 320.84 128.98 336.13 120.49 345.04 107.75 C 354.48 94.4 349.18 69.45 322.48 69.45 C 304.98 69.45 296.39 82.7 292.77 93.67 L 265.94 82.39 C 273.29 60.39 293.27 41.39 322.37 41.39 C 346.69 41.39 363.37 52.47 371.85 66.34 C 379.1 78.25 383.34 100.51 372.16 117.07 C 359.74 135.39 347.83 140.98 341.41 152.79 C 338.83 157.55 337.79 160.65 337.79 175.98 L 307.87 175.98 C 307.77 167.9 306.53 154.75 312.84 143.37 Z M 343.17 217.37 C 343.175 222.063 341.584 226.62 338.661 230.291 C 335.737 233.962 331.651 236.533 327.077 237.579 C 322.502 238.625 317.705 238.086 313.477 236.05 C 309.249 234.015 305.835 230.601 303.8 226.373 C 301.764 222.145 301.225 217.348 302.271 212.773 C 303.317 208.199 305.888 204.113 309.559 201.189 C 313.23 198.266 317.787 196.675 322.48 196.68 C 327.963 196.698 333.221 198.888 337.097 202.767 C 340.972 206.646 343.157 211.907 343.17 217.39 Z" | ||||||
|  |             android:fillColor="#ffffff" | ||||||
|  |             android:strokeWidth="1" /> | ||||||
|  | </vector> | ||||||
							
								
								
									
										32
									
								
								app/src/main/res/drawable/open_subtitles_icon.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/src/main/res/drawable/open_subtitles_icon.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |         android:name="vector" | ||||||
|  |         android:width="24dp" | ||||||
|  |         android:height="24dp" | ||||||
|  |         android:viewportWidth="283" | ||||||
|  |         android:viewportHeight="283" | ||||||
|  |         android:tint="?attr/white"> | ||||||
|  |     <group android:name="group"> | ||||||
|  |         <path | ||||||
|  |                 android:name="path" | ||||||
|  |                 android:pathData="M 16.72 227.55 L 53.82 227.55 L 53.82 264.65 L 16.72 264.65 Z M 70.41 227.55 L 107.51 227.55 L 107.51 264.65 L 70.41 264.65 Z M 123.18 227.55 L 160.28 227.55 L 160.28 264.65 L 123.18 264.65 Z" | ||||||
|  |                 android:fillColor="@color/white" | ||||||
|  |                 android:strokeWidth="1" /> | ||||||
|  |         <path | ||||||
|  |                 android:name="path_1" | ||||||
|  |                 android:pathData="M 123.18 227.55 L 160.28 227.55 L 160.28 264.65 L 123.18 264.65 Z M 176.87 227.55 L 213.97 227.55 L 213.97 264.65 L 176.87 264.65 Z M 229.65 227.55 L 266.75 227.55 L 266.75 264.65 L 229.65 264.65 Z M 16.22 15.49 L 53.32 15.49 L 53.32 52.59 L 16.22 52.59 Z M 69.91 15.49 L 107.01 15.49 L 107.01 52.59 L 69.91 52.59 Z M 122.68 15.49 L 159.78 15.49 L 159.78 52.59 L 122.68 52.59 Z" | ||||||
|  |                 android:fillColor="@color/white" | ||||||
|  |                 android:strokeWidth="1" /> | ||||||
|  |         <path | ||||||
|  |                 android:name="path_2" | ||||||
|  |                 android:pathData="M 122.68 15.49 L 159.78 15.49 L 159.78 52.59 L 122.68 52.59 Z M 176.38 15.49 L 213.48 15.49 L 213.48 52.59 L 176.38 52.59 Z M 229.15 15.49 L 266.25 15.49 L 266.25 52.59 L 229.15 52.59 Z" | ||||||
|  |                 android:fillColor="@color/white" | ||||||
|  |                 android:strokeWidth="1" /> | ||||||
|  |     </group> | ||||||
|  |     <group android:name="text"> | ||||||
|  |         <path | ||||||
|  |                 android:name="path_3" | ||||||
|  |                 android:pathData="M 35 139.88 Q 35 113.69 52.32 96.64 Q 69.64 79.59 93.39 79.58 Q 119.67 79.58 136.86 96.73 Q 154.05 113.88 154.05 140.06 Q 154.05 166.06 137.05 183.26 Q 120.05 200.46 94.2 200.45 Q 68.37 200.45 51.68 183.35 Q 34.99 166.25 35 139.88 Z M 94.57 103.16 Q 79.72 102.89 70.64 113.42 Q 61.56 123.95 61.54 140.6 Q 61.54 156.35 70.81 166.7 C 73.705 170.042 77.303 172.703 81.347 174.493 C 85.39 176.282 89.78 177.155 94.2 177.05 Q 109.05 177.05 118.2 166.83 Q 127.35 156.61 127.33 139.83 Q 127.33 123.36 118.38 113.37 Q 109.43 103.38 94.56 103.16 Z M 245.3 91.55 L 229.46 108.92 Q 216.95 101.54 211.46 101.54 C 210.088 101.531 208.73 101.812 207.474 102.363 C 206.218 102.914 205.092 103.724 204.17 104.74 C 203.182 105.741 202.402 106.928 201.877 108.233 C 201.352 109.537 201.091 110.934 201.11 112.34 Q 201.11 121.07 216.95 127.46 C 223.058 129.928 228.932 132.94 234.5 136.46 C 238.642 139.329 242.048 143.136 244.44 147.57 C 247.095 152.262 248.475 157.569 248.44 162.96 Q 248.44 178.17 236.16 189.42 C 228.316 196.779 217.915 200.814 207.16 200.67 Q 188.8 200.67 170.9 183.39 L 187.63 163.86 Q 198.88 175.47 208.69 175.47 Q 213.28 175.47 217.51 171.38 Q 221.74 167.29 221.74 162.81 Q 221.74 153.57 202.21 146.51 Q 191.05 142.44 186.37 138.89 C 182.948 136.128 180.247 132.576 178.5 128.54 C 176.412 124.199 175.319 119.447 175.3 114.63 Q 175.3 98.88 185.56 89.07 Q 195.82 79.26 212.38 79.27 Q 232 79.23 245.3 91.55 Z" | ||||||
|  |                 android:fillColor="@color/white" | ||||||
|  |                 android:strokeWidth="1" /> | ||||||
|  |     </group> | ||||||
|  | </vector> | ||||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/question_mark_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/drawable/question_mark_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="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z"/> | ||||||
|  | </vector> | ||||||
|  | @ -16,13 +16,14 @@ | ||||||
|             android:layout_height="wrap_content"> |             android:layout_height="wrap_content"> | ||||||
| 
 | 
 | ||||||
|         <androidx.cardview.widget.CardView |         <androidx.cardview.widget.CardView | ||||||
|  |                 android:id="@+id/account_main_profile_picture_holder" | ||||||
|                 app:cardCornerRadius="100dp" |                 app:cardCornerRadius="100dp" | ||||||
|                 android:layout_gravity="center_vertical" |                 android:layout_gravity="center_vertical" | ||||||
|                 android:layout_width="35dp" |                 android:layout_width="35dp" | ||||||
|                 android:layout_height="35dp"> |                 android:layout_height="35dp"> | ||||||
| 
 | 
 | ||||||
|             <ImageView |             <ImageView | ||||||
|                     android:id="@+id/account_profile_picture" |                     android:id="@+id/account_main_profile_picture" | ||||||
|                     android:layout_width="match_parent" |                     android:layout_width="match_parent" | ||||||
|                     android:layout_height="match_parent" |                     android:layout_height="match_parent" | ||||||
|                     tools:ignore="ContentDescription" /> |                     tools:ignore="ContentDescription" /> | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
|         android:layout_width="match_parent"> |         android:layout_width="match_parent"> | ||||||
| 
 | 
 | ||||||
|     <androidx.cardview.widget.CardView |     <androidx.cardview.widget.CardView | ||||||
|  |             android:id="@+id/account_profile_picture_holder" | ||||||
|             android:layout_marginStart="10dp" |             android:layout_marginStart="10dp" | ||||||
|             app:cardCornerRadius="100dp" |             app:cardCornerRadius="100dp" | ||||||
|             android:layout_gravity="center_vertical" |             android:layout_gravity="center_vertical" | ||||||
|  |  | ||||||
							
								
								
									
										127
									
								
								app/src/main/res/layout/add_account_input.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								app/src/main/res/layout/add_account_input.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,127 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |         xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |         xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |         android:orientation="vertical" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent"> | ||||||
|  | 
 | ||||||
|  |     <FrameLayout | ||||||
|  |             android:layout_marginTop="20dp" | ||||||
|  |             android:layout_marginBottom="10dp" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content"> | ||||||
|  |         <TextView | ||||||
|  |                 android:id="@+id/text1" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  | 
 | ||||||
|  |                 android:layout_gravity="center_vertical" | ||||||
|  |                 android:layout_rowWeight="1" | ||||||
|  | 
 | ||||||
|  |                 android:paddingStart="?android:attr/listPreferredItemPaddingStart" | ||||||
|  |                 android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" | ||||||
|  |                 android:textColor="?attr/textColor" | ||||||
|  |                 android:textSize="20sp" | ||||||
|  |                 android:textStyle="bold" | ||||||
|  |                 tools:text="Test" /> | ||||||
|  | 
 | ||||||
|  |         <com.google.android.material.button.MaterialButton | ||||||
|  |                 style="@style/WhiteButton" | ||||||
|  |                 android:layout_gravity="center_vertical|end" | ||||||
|  |                 app:icon="@drawable/ic_baseline_add_24" | ||||||
|  |                 android:text="@string/create_account" | ||||||
|  |                 android:id="@+id/create_account" | ||||||
|  |                 android:layout_width="wrap_content" /> | ||||||
|  |     </FrameLayout> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     <LinearLayout | ||||||
|  |             android:orientation="vertical" | ||||||
|  |             android:layout_marginBottom="60dp" | ||||||
|  |             android:layout_marginHorizontal="10dp" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content"> | ||||||
|  | 
 | ||||||
|  |         <EditText | ||||||
|  |                 android:hint="@string/example_username" | ||||||
|  |                 android:autofillHints="username" | ||||||
|  |                 android:id="@+id/login_username_input" | ||||||
|  |                 android:nextFocusRight="@id/cancel_btt" | ||||||
|  |                 android:nextFocusLeft="@id/apply_btt" | ||||||
|  |                 android:nextFocusDown="@id/login_email_input" | ||||||
|  |                 android:requiresFadingEdge="vertical" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:inputType="text" | ||||||
|  |                 tools:ignore="LabelFor" /> | ||||||
|  | 
 | ||||||
|  |         <EditText | ||||||
|  |                 android:autofillHints="emailAddress" | ||||||
|  |                 android:hint="@string/example_email" | ||||||
|  |                 android:id="@+id/login_email_input" | ||||||
|  |                 android:nextFocusRight="@id/cancel_btt" | ||||||
|  |                 android:nextFocusLeft="@id/apply_btt" | ||||||
|  |                 android:nextFocusUp="@id/login_username_input" | ||||||
|  |                 android:nextFocusDown="@id/login_server_input" | ||||||
|  | 
 | ||||||
|  |                 android:requiresFadingEdge="vertical" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:inputType="textEmailAddress" | ||||||
|  |                 tools:ignore="LabelFor" /> | ||||||
|  | 
 | ||||||
|  |         <EditText | ||||||
|  |                 android:hint="@string/example_ip" | ||||||
|  |                 android:id="@+id/login_server_input" | ||||||
|  |                 android:nextFocusRight="@id/cancel_btt" | ||||||
|  |                 android:nextFocusLeft="@id/apply_btt" | ||||||
|  |                 android:nextFocusUp="@id/login_email_input" | ||||||
|  |                 android:nextFocusDown="@id/login_password_input" | ||||||
|  |                 android:requiresFadingEdge="vertical" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:inputType="textUri" | ||||||
|  |                 tools:ignore="LabelFor" /> | ||||||
|  | 
 | ||||||
|  |         <EditText | ||||||
|  |                 android:hint="@string/example_password" | ||||||
|  |                 android:id="@+id/login_password_input" | ||||||
|  |                 android:nextFocusRight="@id/cancel_btt" | ||||||
|  |                 android:nextFocusLeft="@id/apply_btt" | ||||||
|  |                 android:nextFocusUp="@id/login_server_input" | ||||||
|  |                 android:nextFocusDown="@id/apply_btt" | ||||||
|  | 
 | ||||||
|  |                 android:requiresFadingEdge="vertical" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:inputType="textVisiblePassword" | ||||||
|  |                 tools:ignore="LabelFor" | ||||||
|  |                 android:autofillHints="password" /> | ||||||
|  |     </LinearLayout> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     <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/login" | ||||||
|  |                 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,6 @@ | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|             android:layout_height="match_parent" |             android:layout_height="match_parent" | ||||||
|             android:layout_rowWeight="1" |             android:layout_rowWeight="1" | ||||||
|             android:autofillHints="Autofill Hint" |  | ||||||
|             android:inputType="text" |             android:inputType="text" | ||||||
|             tools:ignore="LabelFor" /> |             tools:ignore="LabelFor" /> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										140
									
								
								app/src/main/res/layout/dialog_online_subtitles.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								app/src/main/res/layout/dialog_online_subtitles.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,140 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |         xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |         xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:background="@null" | ||||||
|  |         android:orientation="vertical"> | ||||||
|  | 
 | ||||||
|  |     <LinearLayout | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="match_parent" | ||||||
|  |             android:layout_marginBottom="60dp" | ||||||
|  |             android:baselineAligned="false" | ||||||
|  |             android:orientation="horizontal"> | ||||||
|  | 
 | ||||||
|  |         <LinearLayout | ||||||
|  |                 android:id="@+id/sort_subtitles_holder" | ||||||
|  |                 android:layout_width="0dp" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:layout_weight="50" | ||||||
|  |                 android:orientation="vertical"> | ||||||
|  | 
 | ||||||
|  |             <!--   android:id="@+id/subs_settings"                 android:foreground="?android:attr/selectableItemBackgroundBorderless" | ||||||
|  | --> | ||||||
|  |             <LinearLayout | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:orientation="horizontal" | ||||||
|  |                     tools:ignore="UseCompoundDrawables"> | ||||||
|  | 
 | ||||||
|  |                 <FrameLayout | ||||||
|  |                         android:layout_width="match_parent" | ||||||
|  |                         android:layout_height="40dp" | ||||||
|  |                         android:layout_margin="10dp" | ||||||
|  |                         android:background="@drawable/search_background" | ||||||
|  |                         android:visibility="visible"> | ||||||
|  | 
 | ||||||
|  |                     <FrameLayout | ||||||
|  |                             android:layout_width="match_parent" | ||||||
|  |                             android:layout_height="30dp" | ||||||
|  |                             android:layout_gravity="center_vertical" | ||||||
|  |                             android:layout_marginEnd="30dp"> | ||||||
|  | 
 | ||||||
|  |                         <androidx.appcompat.widget.SearchView | ||||||
|  |                                 android:id="@+id/subtitles_search" | ||||||
|  |                                 app:iconifiedByDefault="false" | ||||||
|  |                                 app:queryBackground="@color/transparent" | ||||||
|  |                                 app:queryHint="@string/search_hint" | ||||||
|  | 
 | ||||||
|  |                                 app:searchIcon="@drawable/search_icon" | ||||||
|  |                                 android:layout_width="match_parent" | ||||||
|  | 
 | ||||||
|  |                                 android:layout_height="match_parent" | ||||||
|  |                                 android:layout_gravity="center_vertical" | ||||||
|  | 
 | ||||||
|  |                                 android:imeOptions="actionSearch" | ||||||
|  |                                 android:inputType="text" | ||||||
|  |                                 android:paddingStart="-10dp" | ||||||
|  |                                 tools:ignore="RtlSymmetry"> | ||||||
|  | 
 | ||||||
|  |                             <androidx.core.widget.ContentLoadingProgressBar | ||||||
|  |                                     android:id="@+id/search_loading_bar" | ||||||
|  |                                     style="@style/Widget.AppCompat.ProgressBar" | ||||||
|  |                                     android:layout_width="20dp" | ||||||
|  |                                     android:layout_height="20dp" | ||||||
|  |                                     android:layout_gravity="center" | ||||||
|  |                                     android:layout_marginStart="-70dp" | ||||||
|  |                                     android:foregroundTint="@color/white" | ||||||
|  |                                     android:visibility="visible" | ||||||
|  |                                     tools:visibility="visible" | ||||||
|  |                                     android:progressTint="@color/white"> | ||||||
|  | 
 | ||||||
|  |                             </androidx.core.widget.ContentLoadingProgressBar> | ||||||
|  |                             <!--app:queryHint="@string/search_hint" | ||||||
|  |                              android:background="@color/grayBackground" @color/itemBackground | ||||||
|  |                                         app:searchHintIcon="@drawable/search_white" | ||||||
|  |                                         --> | ||||||
|  |                         </androidx.appcompat.widget.SearchView> | ||||||
|  |                     </FrameLayout> | ||||||
|  | 
 | ||||||
|  |                     <ImageView | ||||||
|  |                             android:id="@+id/search_filter" | ||||||
|  |                             app:tint="?attr/textColor" | ||||||
|  |                             android:layout_width="25dp" | ||||||
|  |                             android:layout_height="25dp" | ||||||
|  | 
 | ||||||
|  |                             android:layout_gravity="end|center_vertical" | ||||||
|  |                             android:layout_margin="10dp" | ||||||
|  |                             android:background="?selectableItemBackgroundBorderless" | ||||||
|  |                             android:contentDescription="@string/change_providers_img_des" | ||||||
|  |                             android:nextFocusLeft="@id/main_search" | ||||||
|  |                             android:nextFocusRight="@id/main_search" | ||||||
|  |                             android:nextFocusUp="@id/nav_rail_view" | ||||||
|  |                             android:nextFocusDown="@id/search_autofit_results" | ||||||
|  |                             android:src="@drawable/ic_baseline_tune_24" /> | ||||||
|  |                 </FrameLayout> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             </LinearLayout> | ||||||
|  | 
 | ||||||
|  |             <ListView | ||||||
|  |                     android:id="@+id/subtitle_adapter" | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="match_parent" | ||||||
|  | 
 | ||||||
|  |                     android:layout_rowWeight="1" | ||||||
|  |                     android:background="?attr/primaryBlackBackground" | ||||||
|  |                     android:nextFocusLeft="@id/sort_providers" | ||||||
|  |                     android:nextFocusRight="@id/cancel_btt" | ||||||
|  |                     android:requiresFadingEdge="vertical" | ||||||
|  |                     tools:listfooter="@layout/sort_bottom_footer_add_choice" | ||||||
|  |                     tools:listitem="@layout/sort_bottom_single_choice" /> | ||||||
|  |         </LinearLayout> | ||||||
|  |     </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/sort_apply" /> | ||||||
|  | 
 | ||||||
|  |         <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> | ||||||
|  | @ -70,7 +70,7 @@ | ||||||
| 
 | 
 | ||||||
|             <TextView |             <TextView | ||||||
|                     android:nextFocusUp="@id/settings_lang" |                     android:nextFocusUp="@id/settings_lang" | ||||||
|                     android:nextFocusDown="@id/settings_nginx" |                     android:nextFocusDown="@id/settings_updates" | ||||||
| 
 | 
 | ||||||
|                     android:id="@+id/settings_ui" |                     android:id="@+id/settings_ui" | ||||||
|                     style="@style/SettingsItem" |                     style="@style/SettingsItem" | ||||||
|  | @ -78,14 +78,6 @@ | ||||||
| 
 | 
 | ||||||
|             <TextView |             <TextView | ||||||
|                     android:nextFocusUp="@id/settings_ui" |                     android:nextFocusUp="@id/settings_ui" | ||||||
|                     android:nextFocusDown="@id/settings_updates" |  | ||||||
| 
 |  | ||||||
|                     android:id="@+id/settings_nginx" |  | ||||||
|                     style="@style/SettingsItem" |  | ||||||
|                     android:text="@string/category_nginx" /> |  | ||||||
| 
 |  | ||||||
|             <TextView |  | ||||||
|                     android:nextFocusUp="@id/settings_nginx" |  | ||||||
|                     android:nextFocusDown="@id/settings_credits" |                     android:nextFocusDown="@id/settings_credits" | ||||||
| 
 | 
 | ||||||
|                     android:id="@+id/settings_updates" |                     android:id="@+id/settings_updates" | ||||||
|  |  | ||||||
|  | @ -128,15 +128,6 @@ | ||||||
|             app:popEnterAnim="@anim/enter_anim" |             app:popEnterAnim="@anim/enter_anim" | ||||||
|             app:popExitAnim="@anim/exit_anim" /> |             app:popExitAnim="@anim/exit_anim" /> | ||||||
| 
 | 
 | ||||||
|     <fragment |  | ||||||
|             android:id="@+id/navigation_settings_nginx" |  | ||||||
|             android:label="@string/title_settings" |  | ||||||
|             android:name="com.lagradost.cloudstream3.ui.settings.SettingsNginx" |  | ||||||
|             app:enterAnim="@anim/enter_anim" |  | ||||||
|             app:exitAnim="@anim/exit_anim" |  | ||||||
|             app:popEnterAnim="@anim/enter_anim" |  | ||||||
|             app:popExitAnim="@anim/exit_anim" /> |  | ||||||
| 
 |  | ||||||
|     <fragment |     <fragment | ||||||
|             android:id="@+id/navigation_settings_updates" |             android:id="@+id/navigation_settings_updates" | ||||||
|             android:label="@string/title_settings" |             android:label="@string/title_settings" | ||||||
|  | @ -264,13 +255,6 @@ | ||||||
|             app:exitAnim="@anim/exit_anim" |             app:exitAnim="@anim/exit_anim" | ||||||
|             app:popEnterAnim="@anim/enter_anim" |             app:popEnterAnim="@anim/enter_anim" | ||||||
|             app:popExitAnim="@anim/exit_anim"> |             app:popExitAnim="@anim/exit_anim"> | ||||||
|         <action |  | ||||||
|                 android:id="@+id/action_navigation_settings_to_navigation_settings_nginx" |  | ||||||
|                 app:destination="@id/navigation_settings_nginx" |  | ||||||
|                 app:enterAnim="@anim/enter_anim" |  | ||||||
|                 app:exitAnim="@anim/exit_anim" |  | ||||||
|                 app:popEnterAnim="@anim/enter_anim" |  | ||||||
|                 app:popExitAnim="@anim/exit_anim" /> |  | ||||||
|         <action |         <action | ||||||
|                 android:id="@+id/action_navigation_settings_to_navigation_settings_ui" |                 android:id="@+id/action_navigation_settings_to_navigation_settings_ui" | ||||||
|                 app:destination="@id/navigation_settings_ui" |                 app:destination="@id/navigation_settings_ui" | ||||||
|  |  | ||||||
|  | @ -180,6 +180,7 @@ | ||||||
| 
 | 
 | ||||||
|     <string name="subs_auto_select_language">Auto-Select Language</string> |     <string name="subs_auto_select_language">Auto-Select Language</string> | ||||||
|     <string name="subs_download_languages">Download Languages</string> |     <string name="subs_download_languages">Download Languages</string> | ||||||
|  |     <string name="subs_subtitle_languages">Subtitle Language</string> | ||||||
|     <string name="subs_hold_to_reset_to_default">Hold to reset to default</string> |     <string name="subs_hold_to_reset_to_default">Hold to reset to default</string> | ||||||
|     <string name="subs_import_text" formatted="true">Import fonts by placing them in %s</string> |     <string name="subs_import_text" formatted="true">Import fonts by placing them in %s</string> | ||||||
|     <string name="continue_watching">Continue Watching</string> |     <string name="continue_watching">Continue Watching</string> | ||||||
|  | @ -437,6 +438,13 @@ | ||||||
|     <!-- account stuff --> |     <!-- account stuff --> | ||||||
|     <string name="anilist_key" translatable="false">anilist_key</string> |     <string name="anilist_key" translatable="false">anilist_key</string> | ||||||
|     <string name="mal_key" translatable="false">mal_key</string> |     <string name="mal_key" translatable="false">mal_key</string> | ||||||
|  |     <string name="opensubtitles_key" translatable="false">opensubtitles_key</string> | ||||||
|  |     <string name="nginx_key" translatable="false">nginx_key</string> | ||||||
|  |     <string name="example_password">password123</string> | ||||||
|  |     <string name="example_username">MyCoolUsername</string> | ||||||
|  |     <string name="example_email">hello@world.com</string> | ||||||
|  |     <string name="example_ip">127.0.0.1</string> | ||||||
|  | 
 | ||||||
|     <!-- |     <!-- | ||||||
|     <string name="mal_account_settings" translatable="false">MAL</string> |     <string name="mal_account_settings" translatable="false">MAL</string> | ||||||
|     <string name="anilist_account_settings" translatable="false">AniList</string> |     <string name="anilist_account_settings" translatable="false">AniList</string> | ||||||
|  | @ -451,6 +459,7 @@ | ||||||
|     <string name="login">Login</string> |     <string name="login">Login</string> | ||||||
|     <string name="switch_account">Switch account</string> |     <string name="switch_account">Switch account</string> | ||||||
|     <string name="add_account">Add account</string> |     <string name="add_account">Add account</string> | ||||||
|  |     <string name="create_account">Create account</string> | ||||||
|     <string name="add_sync">Add tracking</string> |     <string name="add_sync">Add tracking</string> | ||||||
|     <string name="added_sync_format" formatted="true">Added %s</string> |     <string name="added_sync_format" formatted="true">Added %s</string> | ||||||
|     <string name="upload_sync">Sync</string> |     <string name="upload_sync">Sync</string> | ||||||
|  | @ -490,6 +499,7 @@ | ||||||
|     <string name="recommended">Recommended</string> |     <string name="recommended">Recommended</string> | ||||||
|     <string name="player_loaded_subtitles" formatted="true">Loaded %s</string> |     <string name="player_loaded_subtitles" formatted="true">Loaded %s</string> | ||||||
|     <string name="player_load_subtitles">Load from file</string> |     <string name="player_load_subtitles">Load from file</string> | ||||||
|  |     <string name="player_load_subtitles_online">Load from Internet</string> | ||||||
|     <string name="downloaded_file">Downloaded file</string> |     <string name="downloaded_file">Downloaded file</string> | ||||||
|     <string name="actor_main">Main</string> |     <string name="actor_main">Main</string> | ||||||
|     <string name="actor_supporting">Supporting</string> |     <string name="actor_supporting">Supporting</string> | ||||||
|  |  | ||||||
|  | @ -357,8 +357,9 @@ | ||||||
| 
 | 
 | ||||||
|     <style name="AlertDialogCustomBlack" parent="Theme.AppCompat.Dialog.Alert"> |     <style name="AlertDialogCustomBlack" parent="Theme.AppCompat.Dialog.Alert"> | ||||||
|         <item name="android:windowBackground">?attr/primaryBlackBackground</item> |         <item name="android:windowBackground">?attr/primaryBlackBackground</item> | ||||||
|         <item name="android:layout_width">fill_parent</item> |         <item name="android:layout_width">match_parent</item> | ||||||
|         <item name="android:layout_height">fill_parent</item> |         <item name="android:layout_height">match_parent</item> | ||||||
|  | 
 | ||||||
|         <!-- No backgrounds, titles or window float --> |         <!-- No backgrounds, titles or window float --> | ||||||
|         <item name="android:windowNoTitle">true</item> |         <item name="android:windowNoTitle">true</item> | ||||||
|         <item name="android:windowIsFloating">false</item> |         <item name="android:windowIsFloating">false</item> | ||||||
|  |  | ||||||
|  | @ -8,6 +8,23 @@ | ||||||
|     <Preference |     <Preference | ||||||
|             android:key="@string/anilist_key" |             android:key="@string/anilist_key" | ||||||
|             android:icon="@drawable/ic_anilist_icon" /> |             android:icon="@drawable/ic_anilist_icon" /> | ||||||
|  |     <Preference | ||||||
|  |             android:key="@string/opensubtitles_key" | ||||||
|  |             android:icon="@drawable/open_subtitles_icon" /> | ||||||
|  |     <Preference | ||||||
|  |             android:key="@string/nginx_key" | ||||||
|  |             android:icon="@drawable/nginx" /> | ||||||
|  | 
 | ||||||
|  |     <Preference | ||||||
|  |             android:key="@string/nginx_info" | ||||||
|  |             android:title="@string/nginx_info_title" | ||||||
|  |             android:icon="@drawable/nginx_question" | ||||||
|  |             android:summary="@string/nginx_info_summary"> | ||||||
|  |         <intent | ||||||
|  |                 android:action="android.intent.action.VIEW" | ||||||
|  |                 android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" /> | ||||||
|  |     </Preference> | ||||||
|  | 
 | ||||||
|     <Preference |     <Preference | ||||||
|             android:key="@string/legal_notice_key" |             android:key="@string/legal_notice_key" | ||||||
|             android:title="@string/legal_notice" |             android:title="@string/legal_notice" | ||||||
|  |  | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|         xmlns:app="http://schemas.android.com/apk/res-auto"> |  | ||||||
|     <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> |  | ||||||
| </PreferenceScreen> |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue