forked from recloudstream/cloudstream
Opensubs dev into master (#1136)
* [Feature][WIP] Add option to download subtitles from Opensubtitles.org (#1082) Co-authored-by: Jace <54625750+Jacekun@users.noreply.github.com> Co-authored-by: Jace <jaceorwell@gmail.com> Co-authored-by: LagradOst <11805592+LagradOst@users.noreply.github.com>
This commit is contained in:
parent
941faf7b5d
commit
918136f8f0
46 changed files with 1499 additions and 499 deletions
|
@ -14,8 +14,8 @@ import com.lagradost.cloudstream3.animeproviders.*
|
|||
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
|
||||
import com.lagradost.cloudstream3.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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -161,7 +161,6 @@ class EgyBestProvider : MainAPI() {
|
|||
@JsonProperty("link") val link: String
|
||||
)
|
||||
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.lagradost.cloudstream3.subtitles
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
|
||||
|
||||
interface AbstractSubProvider {
|
||||
@WorkerThread
|
||||
suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun load(data: SubtitleEntity): String? {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.lagradost.cloudstream3.subtitles
|
||||
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
|
||||
class AbstractSubtitleEntities {
|
||||
data class SubtitleEntity(
|
||||
var idPrefix : String,
|
||||
var name: String = "", //Title of movie/series. This is the one to be displayed when choosing.
|
||||
var lang: String = "en",
|
||||
var data: String = "", //Id or link, depends on provider how to process
|
||||
var type: TvType = TvType.Movie, //Movie, TV series, etc..
|
||||
var epNumber: Int? = null,
|
||||
var seasonNumber: Int? = null,
|
||||
var year: Int? = null
|
||||
)
|
||||
|
||||
data class SubtitleSearch(
|
||||
var query: String = "",
|
||||
var imdb: Long? = null,
|
||||
var lang: String? = null,
|
||||
var epNumber: Int? = null,
|
||||
var seasonNumber: Int? = null,
|
||||
var year: Int? = null
|
||||
)
|
||||
}
|
|
@ -3,9 +3,77 @@ package com.lagradost.cloudstream3.syncproviders
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.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)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package com.lagradost.cloudstream3.syncproviders
|
||||
|
||||
interface AuthAPI {
|
||||
val name: String
|
||||
val icon: Int?
|
||||
|
||||
val requiresLogin: Boolean
|
||||
|
||||
val createAccountUrl : String?
|
||||
|
||||
// don't change this as all keys depend on it
|
||||
val idPrefix: String
|
||||
|
||||
// if this returns null then you are not logged in
|
||||
fun loginInfo(): LoginInfo?
|
||||
fun logOut()
|
||||
|
||||
class LoginInfo(
|
||||
val profilePicture: String? = null,
|
||||
val name: String?,
|
||||
val accountIndex: Int,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package com.lagradost.cloudstream3.syncproviders
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
|
||||
interface InAppAuthAPI : AuthAPI {
|
||||
data class LoginData(
|
||||
val username: String? = null,
|
||||
val password: String? = null,
|
||||
val server: String? = null,
|
||||
val email: String? = null,
|
||||
)
|
||||
|
||||
// this is for displaying the UI
|
||||
val requiresPassword: Boolean
|
||||
val requiresUsername: Boolean
|
||||
val requiresServer: Boolean
|
||||
val requiresEmail: Boolean
|
||||
|
||||
// if this is false we can assume that getLatestLoginData returns null and wont be called
|
||||
// this is used in case for some reason it is not preferred to store any login data besides the "token" or encrypted data
|
||||
val storesPasswordInPlainText: Boolean
|
||||
|
||||
// return true if logged in successfully
|
||||
suspend fun login(data: LoginData): Boolean
|
||||
|
||||
// used to fill the UI if you want to edit any data about your login info
|
||||
fun getLatestLoginData(): LoginData?
|
||||
}
|
||||
|
||||
abstract class InAppAuthAPIManager(defIndex: Int) : AccountManager(defIndex), InAppAuthAPI {
|
||||
override val requiresPassword = false
|
||||
override val requiresUsername = false
|
||||
override val requiresEmail = false
|
||||
override val requiresServer = false
|
||||
override val storesPasswordInPlainText = true
|
||||
override val requiresLogin = true
|
||||
|
||||
// runs on startup
|
||||
@WorkerThread
|
||||
open suspend fun initialize() {
|
||||
}
|
||||
|
||||
override fun logOut() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override val idPrefix: String
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
override val name: String
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
override val icon: Int? = null
|
||||
|
||||
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
|
@ -1,77 +1,9 @@
|
|||
package com.lagradost.cloudstream3.syncproviders
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders
|
|||
import com.lagradost.cloudstream3.*
|
||||
|
||||
interface SyncAPI : OAuth2API {
|
||||
val icon: Int
|
||||
val mainUrl: String
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,10 +12,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.movieproviders.NginxProvider
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
|
||||
|
||||
class NginxApi(index: Int) : InAppAuthAPIManager(index) {
|
||||
override val name = "Nginx"
|
||||
override val idPrefix = "nginx"
|
||||
override val icon = R.drawable.nginx
|
||||
override val requiresUsername = true
|
||||
override val requiresPassword = true
|
||||
override val requiresServer = true
|
||||
override val createAccountUrl = "https://www.sarlays.com/use-nginx-with-cloudstream/"
|
||||
|
||||
companion object {
|
||||
const val NGINX_USER_KEY: String = "nginx_user"
|
||||
}
|
||||
|
||||
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
|
||||
return getKey(accountId, NGINX_USER_KEY)
|
||||
}
|
||||
|
||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||
val data = getLatestLoginData() ?: return null
|
||||
return AuthAPI.LoginInfo(name = data.username ?: data.server, accountIndex = accountIndex)
|
||||
}
|
||||
|
||||
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
|
||||
if (data.server.isNullOrBlank()) return false // we require a server
|
||||
switchToNewAccount()
|
||||
setKey(accountId, NGINX_USER_KEY, data)
|
||||
registerAccount()
|
||||
initialize()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun logOut() {
|
||||
removeAccountKeys()
|
||||
initializeData()
|
||||
}
|
||||
|
||||
private fun initializeData() {
|
||||
val data = getLatestLoginData() ?: run {
|
||||
NginxProvider.overrideUrl = null
|
||||
NginxProvider.loginCredentials = null
|
||||
return
|
||||
}
|
||||
NginxProvider.overrideUrl = data.server?.removeSuffix("/")
|
||||
NginxProvider.loginCredentials = "${data.username ?: ""}:${data.password ?: ""}"
|
||||
}
|
||||
|
||||
override suspend fun initialize() {
|
||||
initializeData()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
|
||||
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubProvider {
|
||||
override val idPrefix = "opensubtitles"
|
||||
override val name = "OpenSubtitles"
|
||||
override val icon = R.drawable.open_subtitles_icon
|
||||
override val requiresPassword = true
|
||||
override val requiresUsername = true
|
||||
override val createAccountUrl = "https://www.opensubtitles.com/"
|
||||
|
||||
companion object {
|
||||
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
|
||||
const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
|
||||
const val host = "https://api.opensubtitles.com/api/v1"
|
||||
const val TAG = "OPENSUBS"
|
||||
const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms
|
||||
var currentCoolDown: Long = 0L
|
||||
var currentSession: SubtitleOAuthEntity? = null
|
||||
}
|
||||
|
||||
private fun canDoRequest(): Boolean {
|
||||
return unixTimeMs > currentCoolDown
|
||||
}
|
||||
|
||||
private fun throwIfCantDoRequest() {
|
||||
if (!canDoRequest()) {
|
||||
throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMs) / 1000L}s")
|
||||
}
|
||||
}
|
||||
|
||||
private fun throwGotTooManyRequests() {
|
||||
currentCoolDown = unixTimeMs + coolDownDuration
|
||||
throw ErrorLoadingException("Too many requests")
|
||||
}
|
||||
|
||||
private fun getAuthKey(): SubtitleOAuthEntity? {
|
||||
return getKey(accountId, OPEN_SUBTITLES_USER_KEY)
|
||||
}
|
||||
|
||||
private fun setAuthKey(data: SubtitleOAuthEntity?) {
|
||||
if (data == null) removeKey(accountId, OPEN_SUBTITLES_USER_KEY)
|
||||
currentSession = data
|
||||
setKey(accountId, OPEN_SUBTITLES_USER_KEY, data)
|
||||
}
|
||||
|
||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||
getAuthKey()?.let { user ->
|
||||
return AuthAPI.LoginInfo(
|
||||
profilePicture = null,
|
||||
name = user.user,
|
||||
accountIndex = accountIndex
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
|
||||
val current = getAuthKey() ?: return null
|
||||
return InAppAuthAPI.LoginData(username = current.user, current.pass)
|
||||
}
|
||||
|
||||
/*
|
||||
Authorize app to connect to API, using username/password.
|
||||
Required to run at startup.
|
||||
Returns OAuth entity with valid access token.
|
||||
*/
|
||||
override suspend fun initialize() {
|
||||
currentSession = getAuthKey() ?: return // just in case the following fails
|
||||
initLogin(currentSession?.user ?: return, currentSession?.pass ?: return)
|
||||
}
|
||||
|
||||
override fun logOut() {
|
||||
setAuthKey(null)
|
||||
removeAccountKeys()
|
||||
currentSession = getAuthKey()
|
||||
}
|
||||
|
||||
private suspend fun initLogin(username: String, password: String): Boolean {
|
||||
Log.i(TAG, "DATA = [$username] [$password]")
|
||||
val response = app.post(
|
||||
url = "$host/login",
|
||||
headers = mapOf(
|
||||
"Api-Key" to apiKey,
|
||||
"Content-Type" to "application/json"
|
||||
),
|
||||
data = mapOf(
|
||||
"username" to username,
|
||||
"password" to password
|
||||
)
|
||||
)
|
||||
Log.i(TAG, "Responsecode = ${response.code}")
|
||||
Log.i(TAG, "Result => ${response.text}")
|
||||
|
||||
if (response.isSuccessful) {
|
||||
AppUtils.tryParseJson<OAuthToken>(response.text)?.let { token ->
|
||||
setAuthKey(
|
||||
SubtitleOAuthEntity(
|
||||
user = username,
|
||||
pass = password,
|
||||
access_token = token.token ?: run {
|
||||
return false
|
||||
})
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
|
||||
val username = data.username ?: throw ErrorLoadingException("Requires Username")
|
||||
val password = data.password ?: throw ErrorLoadingException("Requires Password")
|
||||
switchToNewAccount()
|
||||
try {
|
||||
if (initLogin(username, password)) {
|
||||
registerAccount()
|
||||
return true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
switchToOldAccount()
|
||||
}
|
||||
switchToOldAccount()
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
Fetch subtitles using token authenticated on previous method (see authorize).
|
||||
Returns list of Subtitles which user can select to download (see load).
|
||||
*/
|
||||
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
||||
throwIfCantDoRequest()
|
||||
val imdbId = query.imdb ?: 0
|
||||
val queryText = query.query.replace(" ", "+")
|
||||
val epNum = query.epNumber ?: 0
|
||||
val seasonNum = query.seasonNumber ?: 0
|
||||
val yearNum = query.year ?: 0
|
||||
val epQuery = if (epNum > 0) "&episode_number=$epNum" else ""
|
||||
val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else ""
|
||||
val yearQuery = if (yearNum > 0) "&year=$yearNum" else ""
|
||||
|
||||
val searchQueryUrl = when (imdbId > 0) {
|
||||
//Use imdb_id to search if its valid
|
||||
true -> "$host/subtitles?imdb_id=$imdbId&languages=${query.lang}$yearQuery$epQuery$seasonQuery"
|
||||
false -> "$host/subtitles?query=$queryText&languages=${query.lang}$yearQuery$epQuery$seasonQuery"
|
||||
}
|
||||
|
||||
val req = app.get(
|
||||
url = searchQueryUrl,
|
||||
headers = mapOf(
|
||||
Pair("Api-Key", apiKey),
|
||||
Pair("Content-Type", "application/json")
|
||||
)
|
||||
)
|
||||
Log.i(TAG, "Search Req => ${req.text}")
|
||||
if (!req.isSuccessful) {
|
||||
if (req.code == 429)
|
||||
throwGotTooManyRequests()
|
||||
return null
|
||||
}
|
||||
|
||||
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
|
||||
|
||||
AppUtils.tryParseJson<Results>(req.text)?.let {
|
||||
it.data?.forEach { item ->
|
||||
val attr = item.attributes ?: return@forEach
|
||||
val featureDetails = attr.featDetails
|
||||
//Use any valid name/title in hierarchy
|
||||
val name = featureDetails?.movieName ?: featureDetails?.title
|
||||
?: featureDetails?.parentTitle ?: attr.release ?: ""
|
||||
val lang = attr.language ?: ""
|
||||
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
|
||||
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
||||
val year = featureDetails?.year ?: query.year
|
||||
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
|
||||
//Log.i(TAG, "Result id/name => ${item.id} / $name")
|
||||
item.attributes?.files?.forEach { file ->
|
||||
val resultData = file.fileId?.toString() ?: ""
|
||||
//Log.i(TAG, "Result file => ${file.fileId} / ${file.fileName}")
|
||||
results.add(
|
||||
AbstractSubtitleEntities.SubtitleEntity(
|
||||
idPrefix = this.idPrefix,
|
||||
name = name,
|
||||
lang = lang,
|
||||
data = resultData,
|
||||
type = type,
|
||||
epNumber = resEpNum,
|
||||
seasonNumber = resSeasonNum,
|
||||
year = year
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/*
|
||||
Process data returned from search.
|
||||
Returns string url for the subtitle file.
|
||||
*/
|
||||
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? {
|
||||
throwIfCantDoRequest()
|
||||
|
||||
val req = app.post(
|
||||
url = "$host/download",
|
||||
headers = mapOf(
|
||||
Pair(
|
||||
"Authorization",
|
||||
"Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
|
||||
),
|
||||
Pair("Api-Key", apiKey),
|
||||
Pair("Content-Type", "application/json"),
|
||||
Pair("Accept", "*/*")
|
||||
),
|
||||
data = mapOf(
|
||||
Pair("file_id", data.data)
|
||||
)
|
||||
)
|
||||
Log.i(TAG, "Request result => (${req.code}) ${req.text}")
|
||||
//Log.i(TAG, "Request headers => ${req.headers}")
|
||||
if (req.isSuccessful) {
|
||||
AppUtils.tryParseJson<ResultDownloadLink>(req.text)?.let {
|
||||
val link = it.link ?: ""
|
||||
Log.i(TAG, "Request load link => $link")
|
||||
return link
|
||||
}
|
||||
} else {
|
||||
if (req.code == 429)
|
||||
throwGotTooManyRequests()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
data class SubtitleOAuthEntity(
|
||||
var user: String,
|
||||
var pass: String,
|
||||
var access_token: String,
|
||||
)
|
||||
|
||||
data class OAuthToken(
|
||||
@JsonProperty("token") var token: String? = null,
|
||||
@JsonProperty("status") var status: Int? = null
|
||||
)
|
||||
|
||||
data class Results(
|
||||
@JsonProperty("data") var data: List<ResultData>? = listOf()
|
||||
)
|
||||
|
||||
data class ResultData(
|
||||
@JsonProperty("id") var id: String? = null,
|
||||
@JsonProperty("type") var type: String? = null,
|
||||
@JsonProperty("attributes") var attributes: ResultAttributes? = ResultAttributes()
|
||||
)
|
||||
|
||||
data class ResultAttributes(
|
||||
@JsonProperty("subtitle_id") var subtitleId: String? = null,
|
||||
@JsonProperty("language") var language: String? = null,
|
||||
@JsonProperty("release") var release: String? = null,
|
||||
@JsonProperty("url") var url: String? = null,
|
||||
@JsonProperty("files") var files: List<ResultFiles>? = listOf(),
|
||||
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails()
|
||||
)
|
||||
|
||||
data class ResultFiles(
|
||||
@JsonProperty("file_id") var fileId: Int? = null,
|
||||
@JsonProperty("file_name") var fileName: String? = null
|
||||
)
|
||||
|
||||
data class ResultDownloadLink(
|
||||
@JsonProperty("link") var link: String? = null,
|
||||
@JsonProperty("file_name") var fileName: String? = null,
|
||||
@JsonProperty("requests") var requests: Int? = null,
|
||||
@JsonProperty("remaining") var remaining: Int? = null,
|
||||
@JsonProperty("message") var message: String? = null,
|
||||
@JsonProperty("reset_time") var resetTime: String? = null,
|
||||
@JsonProperty("reset_time_utc") var resetTimeUtc: String? = null
|
||||
)
|
||||
|
||||
data class ResultFeatureDetails(
|
||||
@JsonProperty("year") var year: Int? = null,
|
||||
@JsonProperty("title") var title: String? = null,
|
||||
@JsonProperty("movie_name") var movieName: String? = null,
|
||||
@JsonProperty("imdb_id") var imdbId: Int? = null,
|
||||
@JsonProperty("tmdb_id") var tmdbId: Int? = null,
|
||||
@JsonProperty("season_number") var seasonNumber: Int? = null,
|
||||
@JsonProperty("episode_number") var episodeNumber: Int? = null,
|
||||
@JsonProperty("parent_imdb_id") var parentImdbId: Int? = null,
|
||||
@JsonProperty("parent_title") var parentTitle: String? = null,
|
||||
@JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null,
|
||||
@JsonProperty("parent_feature_id") var parentFeatureId: Int? = null
|
||||
)
|
||||
}
|
|
@ -33,7 +33,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
|||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.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(
|
||||
|
|
|
@ -815,10 +815,6 @@ class CS3IPlayer : IPlayer {
|
|||
null
|
||||
}
|
||||
}
|
||||
SubtitleOrigin.OPEN_SUBTITLES -> {
|
||||
// TODO
|
||||
throw NotImplementedError()
|
||||
}
|
||||
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
|
||||
if (offlineSourceFactory != null) {
|
||||
activeSubtitles.add(sub)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
|
||||
class SettingsNginx : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setUpToolbar(R.string.category_nginx)
|
||||
}
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
hideKeyboard()
|
||||
setPreferencesFromResource(R.xml.settings_nginx, rootKey)
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
getPref(R.string.nginx_credentials)?.setOnPreferenceClickListener {
|
||||
activity?.showNginxTextInputDialog(
|
||||
settingsManager.getString(
|
||||
getString(R.string.nginx_credentials_title),
|
||||
"Nginx Credentials"
|
||||
).toString(),
|
||||
settingsManager.getString(getString(R.string.nginx_credentials), "")
|
||||
.toString(), // key: the actual you use rn
|
||||
android.text.InputType.TYPE_TEXT_VARIATION_URI,
|
||||
{}) {
|
||||
settingsManager.edit()
|
||||
.putString(getString(R.string.nginx_credentials), it)
|
||||
.apply() // change the stored url in nginx_url_key to it
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.nginx_url_key)?.setOnPreferenceClickListener {
|
||||
activity?.showNginxTextInputDialog(
|
||||
settingsManager.getString(getString(R.string.nginx_url_pref), "Nginx server url")
|
||||
.toString(),
|
||||
settingsManager.getString(getString(R.string.nginx_url_key), "")
|
||||
.toString(), // key: the actual you use rn
|
||||
android.text.InputType.TYPE_TEXT_VARIATION_URI, // uri
|
||||
{}) {
|
||||
settingsManager.edit()
|
||||
.putString(getString(R.string.nginx_url_key), it)
|
||||
.apply() // change the stored url in nginx_url_key to it
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,7 +45,6 @@ object SubtitleHelper {
|
|||
* @param looseCheck will use .contains in addition to .equals
|
||||
* */
|
||||
fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? {
|
||||
|
||||
languages.forEach {
|
||||
if (it.languageName.equals(input, ignoreCase = true)
|
||||
|| it.nativeName.equals(input, ignoreCase = true)
|
||||
|
|
13
app/src/main/res/drawable/nginx.xml
Normal file
13
app/src/main/res/drawable/nginx.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/white"
|
||||
android:viewportWidth="283"
|
||||
android:viewportHeight="283">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 253.41 62.61 L 154.22 5.34 C 150.42 3.146 146.108 1.991 141.72 1.991 C 137.332 1.991 133.02 3.146 129.22 5.34 L 30 62.61 C 26.202 64.807 23.049 67.966 20.858 71.768 C 18.668 75.57 17.516 79.882 17.52 84.27 L 17.52 198.8 C 17.516 203.188 18.668 207.5 20.858 211.302 C 23.049 215.104 26.202 218.263 30 220.46 L 129.19 277.72 C 132.99 279.914 137.302 281.069 141.69 281.069 C 146.078 281.069 150.39 279.914 154.19 277.72 L 253.38 220.46 C 257.183 218.266 260.343 215.109 262.539 211.307 C 264.735 207.505 265.891 203.191 265.89 198.8 L 265.89 84.27 C 265.894 79.882 264.742 75.57 262.552 71.768 C 260.361 67.966 257.208 64.807 253.41 62.61 Z M 203.28 185.33 Q 203.28 200.61 187.03 200.61 C 184.56 200.637 182.098 200.331 179.71 199.7 C 177.529 199.086 175.467 198.109 173.61 196.81 C 171.687 195.463 169.917 193.91 168.33 192.18 Q 165.9 189.52 163.45 186.76 L 106.86 119.16 L 106.86 187.16 Q 106.86 193.81 102.86 197.22 C 100.004 199.558 96.388 200.768 92.7 200.62 Q 86.3 200.62 82.44 197.18 Q 78.58 193.74 78.58 187.18 L 78.58 97.63 C 78.438 94.563 78.992 91.503 80.2 88.68 C 81.685 86.126 83.925 84.093 86.61 82.86 C 89.603 81.356 92.911 80.585 96.26 80.61 C 98.633 80.541 101.001 80.879 103.26 81.61 C 105.096 82.243 106.813 83.179 108.34 84.38 C 109.979 85.728 111.477 87.239 112.81 88.89 C 114.33 90.74 115.91 92.66 117.53 94.67 L 175.53 163.06 L 175.53 94.06 Q 175.53 87.34 179.24 83.97 Q 182.95 80.6 189.24 80.61 C 193.57 80.61 197 81.73 199.5 83.97 C 202 86.21 203.26 89.58 203.26 94.06 Z"
|
||||
android:fillColor="#000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
18
app/src/main/res/drawable/nginx_question.xml
Normal file
18
app/src/main/res/drawable/nginx_question.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:name="vector"
|
||||
android:tint="?attr/white"
|
||||
android:width="35dp"
|
||||
android:height="26dp"
|
||||
android:viewportWidth="379"
|
||||
android:viewportHeight="279">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 235.89 60.62 L 136.7 3.35 C 132.9 1.156 128.588 0.001 124.2 0.001 C 119.812 0.001 115.5 1.156 111.7 3.35 L 12.48 60.62 C 8.682 62.817 5.529 65.976 3.338 69.778 C 1.148 73.58 -0.004 77.892 0 82.28 L 0 196.81 C -0.004 201.198 1.148 205.51 3.338 209.312 C 5.529 213.114 8.682 216.273 12.48 218.47 L 111.67 275.73 C 115.47 277.924 119.782 279.079 124.17 279.079 C 128.558 279.079 132.87 277.924 136.67 275.73 L 235.86 218.47 C 239.663 216.276 242.823 213.119 245.019 209.317 C 247.215 205.515 248.371 201.201 248.37 196.81 L 248.37 82.28 C 248.374 77.892 247.222 73.58 245.032 69.778 C 242.841 65.976 239.688 62.817 235.89 60.62 Z M 185.76 183.34 Q 185.76 198.62 169.51 198.62 C 167.04 198.647 164.578 198.341 162.19 197.71 C 160.009 197.096 157.947 196.119 156.09 194.82 C 154.167 193.473 152.397 191.92 150.81 190.19 Q 148.38 187.53 145.93 184.77 L 89.34 117.17 L 89.34 185.17 Q 89.34 191.82 85.34 195.23 C 82.484 197.568 78.868 198.778 75.18 198.63 Q 68.78 198.63 64.92 195.19 Q 61.06 191.75 61.06 185.19 L 61.06 95.64 C 60.918 92.573 61.472 89.513 62.68 86.69 C 64.165 84.136 66.405 82.103 69.09 80.87 C 72.083 79.366 75.391 78.595 78.74 78.62 C 81.113 78.551 83.481 78.889 85.74 79.62 C 87.576 80.253 89.293 81.189 90.82 82.39 C 92.459 83.738 93.957 85.249 95.29 86.9 C 96.81 88.75 98.39 90.67 100.01 92.68 L 158.01 161.07 L 158.01 92.07 Q 158.01 85.35 161.72 81.98 Q 165.43 78.61 171.72 78.62 C 176.05 78.62 179.48 79.74 181.98 81.98 C 184.48 84.22 185.74 87.59 185.74 92.07 Z"
|
||||
android:fillColor="#000"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 312.84 143.37 C 320.84 128.98 336.13 120.49 345.04 107.75 C 354.48 94.4 349.18 69.45 322.48 69.45 C 304.98 69.45 296.39 82.7 292.77 93.67 L 265.94 82.39 C 273.29 60.39 293.27 41.39 322.37 41.39 C 346.69 41.39 363.37 52.47 371.85 66.34 C 379.1 78.25 383.34 100.51 372.16 117.07 C 359.74 135.39 347.83 140.98 341.41 152.79 C 338.83 157.55 337.79 160.65 337.79 175.98 L 307.87 175.98 C 307.77 167.9 306.53 154.75 312.84 143.37 Z M 343.17 217.37 C 343.175 222.063 341.584 226.62 338.661 230.291 C 335.737 233.962 331.651 236.533 327.077 237.579 C 322.502 238.625 317.705 238.086 313.477 236.05 C 309.249 234.015 305.835 230.601 303.8 226.373 C 301.764 222.145 301.225 217.348 302.271 212.773 C 303.317 208.199 305.888 204.113 309.559 201.189 C 313.23 198.266 317.787 196.675 322.48 196.68 C 327.963 196.698 333.221 198.888 337.097 202.767 C 340.972 206.646 343.157 211.907 343.17 217.39 Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
32
app/src/main/res/drawable/open_subtitles_icon.xml
Normal file
32
app/src/main/res/drawable/open_subtitles_icon.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="283"
|
||||
android:viewportHeight="283"
|
||||
android:tint="?attr/white">
|
||||
<group android:name="group">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 16.72 227.55 L 53.82 227.55 L 53.82 264.65 L 16.72 264.65 Z M 70.41 227.55 L 107.51 227.55 L 107.51 264.65 L 70.41 264.65 Z M 123.18 227.55 L 160.28 227.55 L 160.28 264.65 L 123.18 264.65 Z"
|
||||
android:fillColor="@color/white"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 123.18 227.55 L 160.28 227.55 L 160.28 264.65 L 123.18 264.65 Z M 176.87 227.55 L 213.97 227.55 L 213.97 264.65 L 176.87 264.65 Z M 229.65 227.55 L 266.75 227.55 L 266.75 264.65 L 229.65 264.65 Z M 16.22 15.49 L 53.32 15.49 L 53.32 52.59 L 16.22 52.59 Z M 69.91 15.49 L 107.01 15.49 L 107.01 52.59 L 69.91 52.59 Z M 122.68 15.49 L 159.78 15.49 L 159.78 52.59 L 122.68 52.59 Z"
|
||||
android:fillColor="@color/white"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:pathData="M 122.68 15.49 L 159.78 15.49 L 159.78 52.59 L 122.68 52.59 Z M 176.38 15.49 L 213.48 15.49 L 213.48 52.59 L 176.38 52.59 Z M 229.15 15.49 L 266.25 15.49 L 266.25 52.59 L 229.15 52.59 Z"
|
||||
android:fillColor="@color/white"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group android:name="text">
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:pathData="M 35 139.88 Q 35 113.69 52.32 96.64 Q 69.64 79.59 93.39 79.58 Q 119.67 79.58 136.86 96.73 Q 154.05 113.88 154.05 140.06 Q 154.05 166.06 137.05 183.26 Q 120.05 200.46 94.2 200.45 Q 68.37 200.45 51.68 183.35 Q 34.99 166.25 35 139.88 Z M 94.57 103.16 Q 79.72 102.89 70.64 113.42 Q 61.56 123.95 61.54 140.6 Q 61.54 156.35 70.81 166.7 C 73.705 170.042 77.303 172.703 81.347 174.493 C 85.39 176.282 89.78 177.155 94.2 177.05 Q 109.05 177.05 118.2 166.83 Q 127.35 156.61 127.33 139.83 Q 127.33 123.36 118.38 113.37 Q 109.43 103.38 94.56 103.16 Z M 245.3 91.55 L 229.46 108.92 Q 216.95 101.54 211.46 101.54 C 210.088 101.531 208.73 101.812 207.474 102.363 C 206.218 102.914 205.092 103.724 204.17 104.74 C 203.182 105.741 202.402 106.928 201.877 108.233 C 201.352 109.537 201.091 110.934 201.11 112.34 Q 201.11 121.07 216.95 127.46 C 223.058 129.928 228.932 132.94 234.5 136.46 C 238.642 139.329 242.048 143.136 244.44 147.57 C 247.095 152.262 248.475 157.569 248.44 162.96 Q 248.44 178.17 236.16 189.42 C 228.316 196.779 217.915 200.814 207.16 200.67 Q 188.8 200.67 170.9 183.39 L 187.63 163.86 Q 198.88 175.47 208.69 175.47 Q 213.28 175.47 217.51 171.38 Q 221.74 167.29 221.74 162.81 Q 221.74 153.57 202.21 146.51 Q 191.05 142.44 186.37 138.89 C 182.948 136.128 180.247 132.576 178.5 128.54 C 176.412 124.199 175.319 119.447 175.3 114.63 Q 175.3 98.88 185.56 89.07 Q 195.82 79.26 212.38 79.27 Q 232 79.23 245.3 91.55 Z"
|
||||
android:fillColor="@color/white"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</vector>
|
10
app/src/main/res/drawable/question_mark_24.xml
Normal file
10
app/src/main/res/drawable/question_mark_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z"/>
|
||||
</vector>
|
|
@ -16,13 +16,14 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<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" />
|
||||
|
|
|
@ -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"
|
||||
|
|
127
app/src/main/res/layout/add_account_input.xml
Normal file
127
app/src/main/res/layout/add_account_input.xml
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:id="@+id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_rowWeight="1"
|
||||
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Test" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/WhiteButton"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
app:icon="@drawable/ic_baseline_add_24"
|
||||
android:text="@string/create_account"
|
||||
android:id="@+id/create_account"
|
||||
android:layout_width="wrap_content" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText
|
||||
android:hint="@string/example_username"
|
||||
android:autofillHints="username"
|
||||
android:id="@+id/login_username_input"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusDown="@id/login_email_input"
|
||||
android:requiresFadingEdge="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<EditText
|
||||
android:autofillHints="emailAddress"
|
||||
android:hint="@string/example_email"
|
||||
android:id="@+id/login_email_input"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusUp="@id/login_username_input"
|
||||
android:nextFocusDown="@id/login_server_input"
|
||||
|
||||
android:requiresFadingEdge="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textEmailAddress"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<EditText
|
||||
android:hint="@string/example_ip"
|
||||
android:id="@+id/login_server_input"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusUp="@id/login_email_input"
|
||||
android:nextFocusDown="@id/login_password_input"
|
||||
android:requiresFadingEdge="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<EditText
|
||||
android:hint="@string/example_password"
|
||||
android:id="@+id/login_password_input"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusUp="@id/login_server_input"
|
||||
android:nextFocusDown="@id/apply_btt"
|
||||
|
||||
android:requiresFadingEdge="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textVisiblePassword"
|
||||
tools:ignore="LabelFor"
|
||||
android:autofillHints="password" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/apply_btt_holder"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="bottom|end"
|
||||
android:layout_marginTop="-60dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/WhiteButton"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/login"
|
||||
android:id="@+id/apply_btt"
|
||||
android:layout_width="wrap_content" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/BlackButton"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_cancel"
|
||||
android:id="@+id/cancel_btt"
|
||||
android:layout_width="wrap_content" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -32,7 +32,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:autofillHints="Autofill Hint"
|
||||
android:inputType="text"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
|
|
140
app/src/main/res/layout/dialog_online_subtitles.xml
Normal file
140
app/src/main/res/layout/dialog_online_subtitles.xml
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sort_subtitles_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="50"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- android:id="@+id/subs_settings" android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="10dp"
|
||||
android:background="@drawable/search_background"
|
||||
android:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="30dp">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/subtitles_search"
|
||||
app:iconifiedByDefault="false"
|
||||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
android:paddingStart="-10dp"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/search_loading_bar"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="-70dp"
|
||||
android:foregroundTint="@color/white"
|
||||
android:visibility="visible"
|
||||
tools:visibility="visible"
|
||||
android:progressTint="@color/white">
|
||||
|
||||
</androidx.core.widget.ContentLoadingProgressBar>
|
||||
<!--app:queryHint="@string/search_hint"
|
||||
android:background="@color/grayBackground" @color/itemBackground
|
||||
app:searchHintIcon="@drawable/search_white"
|
||||
-->
|
||||
</androidx.appcompat.widget.SearchView>
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/search_filter"
|
||||
app:tint="?attr/textColor"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_margin="10dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/change_providers_img_des"
|
||||
android:nextFocusLeft="@id/main_search"
|
||||
android:nextFocusRight="@id/main_search"
|
||||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/search_autofit_results"
|
||||
android:src="@drawable/ic_baseline_tune_24" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/subtitle_adapter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:nextFocusLeft="@id/sort_providers"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:listfooter="@layout/sort_bottom_footer_add_choice"
|
||||
tools:listitem="@layout/sort_bottom_single_choice" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/apply_btt_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginTop="-60dp"
|
||||
android:gravity="bottom|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/apply_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_apply" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_cancel" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -70,7 +70,7 @@
|
|||
|
||||
<TextView
|
||||
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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<Preference
|
||||
android:key="@string/nginx_url_key"
|
||||
android:title="@string/nginx_url_pref"
|
||||
android:icon="@drawable/ic_baseline_play_arrow_24" />
|
||||
<Preference
|
||||
android:key="@string/nginx_credentials"
|
||||
android:title="@string/nginx_credentials_title"
|
||||
android:icon="@drawable/video_locked"
|
||||
android:summary="@string/nginx_credentials_summary" />
|
||||
<Preference
|
||||
android:key="@string/nginx_info"
|
||||
android:title="@string/nginx_info_title"
|
||||
android:icon="@drawable/ic_baseline_play_arrow_24"
|
||||
android:summary="@string/nginx_info_summary">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" />
|
||||
</Preference>
|
||||
</PreferenceScreen>
|
Loading…
Reference in a new issue