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:
LagradOst 2022-06-07 18:38:24 +02:00 committed by GitHub
parent 941faf7b5d
commit 918136f8f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1499 additions and 499 deletions

View File

@ -14,8 +14,8 @@ import com.lagradost.cloudstream3.animeproviders.*
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
import com.lagradost.cloudstream3.movieproviders.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink
@ -352,7 +352,8 @@ abstract class MainAPI {
fun overrideWithNewData(data: ProvidersInfoJson) {
this.name = data.name
this.mainUrl = data.url
if (data.url.isNotBlank() && data.url != "NONE")
this.mainUrl = data.url
this.storedCredentials = data.credentials
}

View File

@ -35,13 +35,13 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.movieproviders.NginxProvider
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accountApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
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.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.result.ResultFragment
@ -132,7 +132,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_download_child,
R.id.navigation_subtitles,
R.id.navigation_chrome_subtitles,
R.id.navigation_settings_nginx,
R.id.navigation_settings_player,
R.id.navigation_settings_updates,
R.id.navigation_settings_ui,
@ -368,10 +367,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onCreate(savedInstanceState: Bundle?) {
// init accounts
for (api in OAuth2accountApis) {
for (api in accountManagers) {
api.init()
}
ioSafe {
inAppAuths.apmap { api ->
try {
api.initialize()
} catch (e: Exception) {
logError(e)
}
}
}
SearchResultBuilder.updateCache(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -391,68 +400,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
false
}
fun addNginxToJson(data: java.util.HashMap<String, ProvidersInfoJson>): java.util.HashMap<String, ProvidersInfoJson> {
try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val nginxUrl =
settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key")
.toString()
val nginxCredentials =
settingsManager.getString(
getString(R.string.nginx_credentials),
"nginx_credentials"
)
.toString()
val storedNginxProvider = NginxProvider()
if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value, or empty:
data[storedNginxProvider.javaClass.simpleName] = ProvidersInfoJson(
url = nginxUrl,
name = storedNginxProvider.name,
status = PROVIDER_STATUS_DOWN, // the provider will not be display
credentials = nginxCredentials
)
} else { // valid url
data[storedNginxProvider.javaClass.simpleName] = ProvidersInfoJson(
url = nginxUrl,
name = storedNginxProvider.name,
status = PROVIDER_STATUS_OK,
credentials = nginxCredentials
)
}
return data
} catch (e: Exception) {
logError(e)
return data
}
}
fun createNginxJson(): ProvidersInfoJson? { //java.util.HashMap<String, ProvidersInfoJson>
return try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val nginxUrl =
settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key")
.toString()
val nginxCredentials = settingsManager.getString(
getString(R.string.nginx_credentials),
"nginx_credentials"
).toString()
if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value or empty:
null // don't overwrite anything
} else {
ProvidersInfoJson(
url = nginxUrl,
name = NginxProvider().name,
status = PROVIDER_STATUS_OK,
credentials = nginxCredentials
)
}
} catch (e: Exception) {
logError(e)
null
}
}
// this pulls the latest data so ppl don't have to update to simply change provider url
if (downloadFromGithub) {
try {
@ -472,11 +419,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
setKey(PROVIDER_STATUS_KEY, txt)
MainAPI.overrideData = newCache // update all new providers
val newUpdatedCache =
newCache?.let { addNginxToJson(it) }
initAll()
for (api in apis) { // update current providers
newUpdatedCache?.get(api.javaClass.simpleName)
newCache?.get(api.javaClass.simpleName)
?.let { data ->
api.overrideWithNewData(data)
}
@ -494,15 +439,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
newCache
}?.let { providersJsonMap ->
MainAPI.overrideData = providersJsonMap
val providersJsonMapUpdated =
addNginxToJson(providersJsonMap) // if return null, use unchanged one
initAll()
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()
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()
apis = allProviders.filter { api ->
@ -527,16 +470,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} else {
initAll()
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)

View File

@ -1,9 +1,9 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil
object SyncRedirector {

View File

@ -3,7 +3,7 @@ package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
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.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
@ -15,7 +15,7 @@ class MultiAnimeProvider : MainAPI() {
override val lang = "en"
override val usesWebView = true
override val supportedTypes = setOf(TvType.Anime)
private val syncApi: SyncAPI = OAuth2API.aniListApi
private val syncApi: SyncAPI = aniListApi
private val syncUtilType by lazy {
when (syncApi) {

View File

@ -161,7 +161,6 @@ class EgyBestProvider : MainAPI() {
@JsonProperty("link") val link: String
)
override suspend fun loadLinks(
data: String,
isCasting: Boolean,

View File

@ -1,13 +1,8 @@
package com.lagradost.cloudstream3.movieproviders
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.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Qualities
import java.lang.Exception
class NginxProvider : MainAPI() {
override var name = "Nginx"
@ -15,23 +10,40 @@ class NginxProvider : MainAPI() {
override val hasMainPage = true
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"
}
fun getAuthHeader(storedCredentials: String?): Map<String, String> {
if (storedCredentials == null) {
return mapOf(Pair("Authorization", "Basic ")) // no Authorization headers
private fun getAuthHeader(): Map<String, String> {
val url = overrideUrl ?: throw ErrorLoadingException(ERROR_STRING)
mainUrl = url
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 {
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
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
@ -44,27 +56,34 @@ class NginxProvider : MainAPI() {
if (isMovie) {
val poster = metadataDocument.selectFirst("thumb")!!.text()
val trailer = metadataDocument.select("trailer").mapNotNull {
it?.text()?.replace(
"plugin://plugin.video.youtube/play/?video_id=",
"https://www.youtube.com/watch?v="
)
it?.text()?.replace(
"plugin://plugin.video.youtube/play/?video_id=",
"https://www.youtube.com/watch?v="
)
}
val partialUrl = mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href").replace(".nfo", ".")
val partialUrl =
mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href")
.replace(".nfo", ".")
val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull()
val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull()
val tagsList = metadataDocument.select("genre")
?.mapNotNull { // all the tags like action, thriller ...
.mapNotNull { // all the tags like action, thriller ...
it?.text()
}
val dataList = mediaRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
"href",
partialUrl
)
val dataList =
mainRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
"href",
partialUrl
)
val data = url + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file
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(
title,
@ -81,7 +100,6 @@ class NginxProvider : MainAPI() {
}
} else // a tv serie
{
val list = ArrayList<Pair<Int, String>>()
val mediaRootUrl = url.replace("tvshow.nfo", "")
val posterUrl = mediaRootUrl + "poster.jpg"
@ -91,7 +109,7 @@ class NginxProvider : MainAPI() {
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()
}
@ -102,7 +120,7 @@ class NginxProvider : MainAPI() {
seasons.forEach { element ->
val season =
element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull()
element.attr("href").replace("Season%20", "").replace("/", "").toIntOrNull()
val href = mediaRootUrl + element.attr("href")
if (season != null && season > 0 && href.isNotBlank()) {
list.add(Pair(season, href))
@ -120,33 +138,40 @@ class NginxProvider : MainAPI() {
"href",
".nfo"
) // get metadata
episodes.forEach { episode ->
val nfoDocument = app.get(seasonString + episode.attr("href"), authHeader).document // get episode metadata file
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
val poster =
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
val name = nfoDocument.selectFirst("title")!!.text()
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull()
val date = nfoDocument.selectFirst("aired")?.text()
val plot = nfoDocument.selectFirst("plot")?.text()
episodes.forEach { episode ->
val nfoDocument = app.get(
seasonString + episode.attr("href"),
authHeader
).document // get episode metadata file
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
val poster =
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
val name = nfoDocument.selectFirst("title")!!.text()
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull()
val date = nfoDocument.selectFirst("aired")?.text()
val plot = nfoDocument.selectFirst("plot")?.text()
val dataList = seasonDocument.getElementsByAttributeValueContaining(
"href",
episode.attr("href").replace(".nfo", "")
)
val data = seasonString + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file
val dataList = seasonDocument.getElementsByAttributeValueContaining(
"href",
episode.attr("href").replace(".nfo", "")
)
val data = seasonString + dataList.firstNotNullOf { item ->
item.takeIf {
(!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))
}
}.attr("href").toString() // exclude poster and nfo (metadata) file
episodeList.add(
newEpisode(data) {
this.name = name
this.season = seasonInt
this.episode = epNum
this.posterUrl = poster // will require headers too
this.description = plot
addDate(date)
}
)
}
episodeList.add(
newEpisode(data) {
this.name = name
this.season = seasonInt
this.episode = epNum
this.posterUrl = poster // will require headers too
this.description = plot
addDate(date)
}
)
}
}
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) {
this.name = title
@ -168,8 +193,9 @@ class NginxProvider : MainAPI() {
callback: (ExtractorLink) -> Unit
): Boolean {
// loadExtractor(data, null) { callback(it.copy(headers=authHeader)) }
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
callback.invoke (
val authHeader =
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
callback.invoke(
ExtractorLink(
name,
name,
@ -185,19 +211,23 @@ class NginxProvider : MainAPI() {
}
override suspend fun getMainPage(): HomePageResponse {
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
if (mainUrl == "NONE"){
throw ErrorLoadingException("No nginx url specified in the settings: Nginx Settigns > Nginx server url, try again in a few seconds")
}
val authHeader =
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
val document = app.get(mainUrl, authHeader).document
val categories = document.select("a")
val returnList = categories.mapNotNull {
val categoryPath = mainUrl + it.attr("href") ?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/
val categoryTitle = it.text() // get the category title like Movies or Series
if (categoryTitle != "../" && categoryTitle != "Music/") { // exclude parent dir and Music dir
val categoryDocument = app.get(categoryPath, authHeader).document // queries the page http://192.168.1.10/media/Movies/
val 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 currentList = contentLinks.mapNotNull { head ->
if (head.attr("href") != "../") {
@ -215,7 +245,6 @@ class NginxProvider : MainAPI() {
val nfoContent =
app.get(nfoPath, authHeader).document // all the metadata
if (isMovieType) {
val movieName = nfoContent.select("title").text()
val posterUrl = mediaRootUrl + "poster.jpg"
@ -238,15 +267,11 @@ class NginxProvider : MainAPI() {
) {
addPoster(posterUrl, authHeader)
}
}
} catch (e: Exception) { // can cause issues invisible errors
null
//logError(e) // not working because it changes the return type of currentList to Any
}
} else null
}
if (currentList.isNotEmpty() && categoryTitle != "../") { // exclude upper dir

View File

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

View File

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

View File

@ -3,9 +3,77 @@ package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
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
private var lastAccountIndex = defIndex
protected val accountId get() = "${idPrefix}_account_$accountIndex"
private val accountActiveKey get() = "${idPrefix}_active"
@ -35,8 +103,12 @@ abstract class AccountManager(private val defIndex: Int) : OAuth2API {
protected fun switchToNewAccount() {
val accounts = getAccounts()
lastAccountIndex = accountIndex
accountIndex = (accounts?.maxOrNull() ?: 0) + 1
}
protected fun switchToOldAccount() {
accountIndex = lastAccountIndex
}
protected fun registerAccount() {
setKey(accountActiveKey, accountIndex)

View File

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

View File

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

View File

@ -1,77 +1,9 @@
package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import java.util.concurrent.TimeUnit
interface OAuth2API {
interface OAuth2API : AuthAPI {
val key: String
val name: String
val redirectUrl: String
// don't change this as all keys depend on it
val idPrefix: String
suspend fun handleRedirect(url: String) : Boolean
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"
}
}
}

View File

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.*
interface SyncAPI : OAuth2API {
val icon: Int
val mainUrl: String
/**

View File

@ -12,10 +12,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API
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.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson
@ -32,11 +29,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val idPrefix = "anilist"
override var mainUrl = "https://anilist.co"
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)?.
getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo(
return AuthAPI.LoginInfo(
profilePicture = user.picture,
name = user.name,
accountIndex = accountIndex

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API
//TODO dropbox sync
@ -8,6 +9,11 @@ class Dropbox : OAuth2API {
override var name = "Dropbox"
override val key = "zlqsamadlwydvb2"
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() {
TODO("Not yet implemented")
@ -21,7 +27,7 @@ class Dropbox : OAuth2API {
TODO("Not yet implemented")
}
override fun loginInfo(): OAuth2API.LoginInfo? {
override fun loginInfo(): AuthAPI.LoginInfo? {
TODO("Not yet implemented")
}
}

View File

@ -14,10 +14,7 @@ import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API
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.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
@ -37,15 +34,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val idPrefix = "mal"
override var mainUrl = "https://myanimelist.net"
override val icon = R.drawable.mal_logo
override val requiresLogin = true
override val createAccountUrl = "$mainUrl/register.php"
override fun logOut() {
removeAccountKeys()
}
override fun loginInfo(): OAuth2API.LoginInfo? {
override fun loginInfo(): AuthAPI.LoginInfo? {
//getMalUser(true)?
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo(
return AuthAPI.LoginInfo(
profilePicture = user.picture,
name = user.name,
accountIndex = accountIndex

View File

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

View File

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

View File

@ -33,7 +33,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
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.randomApi
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
@ -882,7 +882,7 @@ class HomeFragment : Fragment() {
home_change_api_loading?.isVisible = false
}
for (syncApi in OAuth2API.OAuth2Apis) {
for (syncApi in OAuth2Apis) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture
if (home_profile_picture?.setImage(

View File

@ -815,10 +815,6 @@ class CS3IPlayer : IPlayer {
null
}
}
SubtitleOrigin.OPEN_SUBTITLES -> {
// TODO
throw NotImplementedError()
}
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)

View File

@ -1,7 +1,10 @@
package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.util.Log
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.normalSafeApiCall
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.PlayerSubtitleHelper.Companion.toSubtitleMimeType
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.settings.SettingsFragment.Companion.isTvSettings
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.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
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.hideSystemUI
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.player_custom_layout.*
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 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() {
try {
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
private val subsPathPicker =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
@ -208,23 +415,7 @@ class GeneratorPlayer : FullScreenPlayer() {
name.toSubtitleMimeType()
)
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), name),
Toast.LENGTH_LONG
)
addAndSelectSubtitles(subtitleData)
}
}
@ -232,6 +423,7 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun showMirrorsDialogue() {
try {
currentSelectedSubtitles = player.getCurrentPreferredSubtitle()
println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs")
context?.let { ctx ->
val isPlaying = player.getIsPlaying()
player.handleEvent(CSPlayerEvent.Pause)
@ -246,13 +438,43 @@ class GeneratorPlayer : FullScreenPlayer() {
val providerList = sourceDialog.sort_providers
val subtitleList = sourceDialog.sort_subtitles
val footer: TextView =
val loadFromFileFooter: 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()
}
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 startSource = 0
@ -283,15 +505,6 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
var shouldDismiss = true
fun dismiss() {
if (isPlaying) {
player.handleEvent(CSPlayerEvent.Play)
}
activity?.hideSystemUI()
}
sourceDialog.setOnDismissListener {
if (shouldDismiss) dismiss()
selectSourceDialog = null
@ -598,18 +811,15 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
@SuppressLint("SetTextI18n")
fun setTitle() {
private fun getPlayerVideoTitle(): String {
var headerName: String? = null
var subName: String? = null
var episode: Int? = null
var season: Int? = null
var tvType: TvType? = null
var isFiller: Boolean? = null
when (val meta = currentMeta) {
is ResultEpisode -> {
isFiller = meta.isFiller
headerName = meta.headerName
subName = meta.name
episode = meta.episode
@ -626,39 +836,40 @@ class GeneratorPlayer : FullScreenPlayer() {
}
//Generate video title
context?.let { ctx ->
var playerVideoTitle = if (headerName != null) {
(headerName +
if (tvType.isEpisodeBased() && episode != null)
if (season == null)
" - ${ctx.getString(R.string.episode)} $episode"
else
" \"${ctx.getString(R.string.season_short)}${season}:${
ctx.getString(
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
val playerVideoTitle = if (headerName != null) {
(headerName +
if (tvType.isEpisodeBased() && episode != null)
if (season == null)
" - ${getString(R.string.episode)} $episode"
else
" \"${getString(R.string.season_short)}${season}:${getString(R.string.episode_short)}${episode}\""
else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName"
} else {
""
}
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")

View File

@ -1,13 +1,10 @@
package com.lagradost.cloudstream3.ui.player
import android.content.Context
import android.net.Uri
import android.util.TypedValue
import android.view.ViewGroup
import android.widget.FrameLayout
import com.google.android.exoplayer2.ui.SubtitleView
import com.google.android.exoplayer2.util.MimeTypes
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveCaptions
@ -24,7 +21,6 @@ enum class SubtitleStatus {
enum class SubtitleOrigin {
URL,
DOWNLOADED_FILE,
OPEN_SUBTITLES,
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 {
return SubtitleData(
name = subtitleFile.lang,

View File

@ -24,7 +24,6 @@ import android.widget.*
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isGone
import androidx.core.view.isVisible
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.material.button.MaterialButton
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
@ -1353,7 +1351,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
val newList = list.filter { it.isSynced && it.hasAccount }
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) {

View File

@ -7,9 +7,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.SyncUtil
import kotlinx.coroutines.launch
@ -20,7 +20,7 @@ data class CurrentSynced(
val idPrefix: String,
val isSynced: Boolean,
val hasAccount: Boolean,
val icon: Int,
val icon: Int?,
)
class SyncViewModel : ViewModel() {

View File

@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.ui.search
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
class SyncSearchViewModel {
private val repos = OAuth2API.SyncApis
private val repos = SyncApis
data class SyncSearchResultSearchResponse(
override val name: String,
@ -18,5 +18,4 @@ class SyncSearchViewModel {
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null,
) : SearchResponse
}

View File

@ -8,13 +8,13 @@ import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.AuthAPI
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(
val cardList: List<OAuth2API.LoginInfo>,
val cardList: List<AuthAPI.LoginInfo>,
val layout: Int = R.layout.account_single,
private val clickCallback: (AccountClickCallback) -> Unit
) :
@ -48,15 +48,13 @@ class AccountAdapter(
private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
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
accountName.text = card.name ?: "%s %d".format(accountName.context.getString(R.string.account), card.accountIndex)
if(card.profilePicture.isNullOrEmpty()) {
pfp.isVisible = false
} else {
pfp.isVisible = true
pfp.setImage(card.profilePicture)
}
accountName.text = card.name ?: "%s %d".format(
accountName.context.getString(R.string.account),
card.accountIndex
)
pfp.isVisible = pfp.setImage(card.profilePicture)
itemView.setOnClickListener {
clickCallback.invoke(AccountClickCallback(0, itemView, card))

View File

@ -1,38 +1,56 @@
package com.lagradost.cloudstream3.ui.settings
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.UiThread
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
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.ui.settings.SettingsFragment.Companion.beneneCount
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
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.hideKeyboard
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() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_credits)
}
private fun showLoginInfo(api: AccountManager, info: OAuth2API.LoginInfo) {
private fun showLoginInfo(api: AccountManager, info: AuthAPI.LoginInfo) {
val builder =
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
.setView(R.layout.account_managment)
val dialog = builder.show()
dialog.findViewById<ImageView>(R.id.account_profile_picture)?.setImage(info.profilePicture)
dialog.findViewById<TextView>(R.id.account_logout)?.setOnClickListener {
dialog.account_main_profile_picture_holder?.isVisible =
dialog.account_main_profile_picture?.setImage(info.profilePicture) == true
dialog.account_logout?.setOnClickListener {
api.logOut()
dialog.dismissSafe(activity)
}
@ -40,13 +58,93 @@ class SettingsAccount : PreferenceFragmentCompat() {
(info.name ?: context?.getString(R.string.no_data))?.let {
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)
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) {
val accounts = api.getAccounts() ?: return
@ -54,17 +152,14 @@ class SettingsAccount : PreferenceFragmentCompat() {
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch)
val dialog = builder.show()
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener {
try {
api.authenticate()
} catch (e: Exception) {
logError(e)
}
dialog.account_add?.setOnClickListener {
addAccount(api)
dialog?.dismissSafe(activity)
}
val ogIndex = api.accountIndex
val items = ArrayList<OAuth2API.LoginInfo>()
val items = ArrayList<AuthAPI.LoginInfo>()
for (index in accounts) {
api.accountIndex = index
@ -97,33 +192,28 @@ class SettingsAccount : PreferenceFragmentCompat() {
val syncApis =
listOf(
Pair(R.string.mal_key, OAuth2API.malApi), Pair(
R.string.anilist_key,
OAuth2API.aniListApi
)
R.string.mal_key to malApi,
R.string.anilist_key to aniListApi,
R.string.opensubtitles_key to openSubtitlesApi,
R.string.nginx_key to nginxApi,
)
for ((key, api) in syncApis) {
getPref(key)?.apply {
title =
getString(R.string.login_format).format(api.name, getString(R.string.account))
setOnPreferenceClickListener { _ ->
setOnPreferenceClickListener {
val info = api.loginInfo()
if (info != null) {
showLoginInfo(api, info)
} else {
try {
api.authenticate()
} catch (e: Exception) {
logError(e)
}
addAccount(api)
}
return@setOnPreferenceClickListener true
}
}
}
try {
beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0)
getPref(R.string.benene_count)?.let { pref ->

View File

@ -16,7 +16,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
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.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate
@ -116,7 +116,7 @@ class SettingsFragment : Fragment() {
val isTrueTv = context?.isTrueTvSettings() == true
for (syncApi in OAuth2API.OAuth2Apis) {
for (syncApi in accountManagers) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue
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_ui, R.id.action_navigation_settings_to_navigation_settings_ui),
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),
).forEach { (view, navigationId) ->
view?.apply {

View File

@ -116,7 +116,7 @@ class SettingsLang : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true
}
getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref ->
getPref(R.string.locale_key)?.setOnPreferenceClickListener {
val tempLangs = languages.toMutableList()
//if (beneneCount > 100) {
// tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo"))

View File

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

View File

@ -45,7 +45,6 @@ object SubtitleHelper {
* @param looseCheck will use .contains in addition to .equals
* */
fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? {
languages.forEach {
if (it.languageName.equals(input, ignoreCase = true)
|| it.nativeName.equals(input, ignoreCase = true)

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

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

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

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

View File

@ -16,13 +16,14 @@
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/account_main_profile_picture_holder"
app:cardCornerRadius="100dp"
android:layout_gravity="center_vertical"
android:layout_width="35dp"
android:layout_height="35dp">
<ImageView
android:id="@+id/account_profile_picture"
android:id="@+id/account_main_profile_picture"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />

View File

@ -7,6 +7,7 @@
android:layout_width="match_parent">
<androidx.cardview.widget.CardView
android:id="@+id/account_profile_picture_holder"
android:layout_marginStart="10dp"
app:cardCornerRadius="100dp"
android:layout_gravity="center_vertical"

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

View File

@ -32,7 +32,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1"
android:autofillHints="Autofill Hint"
android:inputType="text"
tools:ignore="LabelFor" />

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

View File

@ -70,7 +70,7 @@
<TextView
android:nextFocusUp="@id/settings_lang"
android:nextFocusDown="@id/settings_nginx"
android:nextFocusDown="@id/settings_updates"
android:id="@+id/settings_ui"
style="@style/SettingsItem"
@ -78,14 +78,6 @@
<TextView
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:id="@+id/settings_updates"

View File

@ -128,15 +128,6 @@
app:popEnterAnim="@anim/enter_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
android:id="@+id/navigation_settings_updates"
android:label="@string/title_settings"
@ -264,13 +255,6 @@
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim">
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_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
android:id="@+id/action_navigation_settings_to_navigation_settings_ui"
app:destination="@id/navigation_settings_ui"

View File

@ -180,6 +180,7 @@
<string name="subs_auto_select_language">Auto-Select Language</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_import_text" formatted="true">Import fonts by placing them in %s</string>
<string name="continue_watching">Continue Watching</string>
@ -437,6 +438,13 @@
<!-- account stuff -->
<string name="anilist_key" translatable="false">anilist_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="anilist_account_settings" translatable="false">AniList</string>
@ -451,6 +459,7 @@
<string name="login">Login</string>
<string name="switch_account">Switch 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="added_sync_format" formatted="true">Added %s</string>
<string name="upload_sync">Sync</string>
@ -490,6 +499,7 @@
<string name="recommended">Recommended</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_online">Load from Internet</string>
<string name="downloaded_file">Downloaded file</string>
<string name="actor_main">Main</string>
<string name="actor_supporting">Supporting</string>

View File

@ -357,8 +357,9 @@
<style name="AlertDialogCustomBlack" parent="Theme.AppCompat.Dialog.Alert">
<item name="android:windowBackground">?attr/primaryBlackBackground</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">fill_parent</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
<!-- No backgrounds, titles or window float -->
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">false</item>

View File

@ -8,6 +8,23 @@
<Preference
android:key="@string/anilist_key"
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
android:key="@string/legal_notice_key"
android:title="@string/legal_notice"

View File

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