removed context (no user change) + small bug fixes

This commit is contained in:
LagradOst 2021-12-17 00:45:20 +01:00
parent 2580455cd5
commit a96b396307
28 changed files with 525 additions and 468 deletions

View File

@ -33,7 +33,7 @@ android {
defaultConfig {
applicationId "com.lagradost.cloudstream3"
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 30
versionCode 37
versionName "2.4.5"

View File

@ -5,7 +5,13 @@ import android.content.Context
import android.widget.Toast
import com.google.auto.service.AutoService
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
import org.acra.ReportField
import org.acra.config.CoreConfiguration
import org.acra.data.CrashReportData
@ -84,5 +90,49 @@ class AcraApplication : Application() {
private set(value) {
_context = WeakReference(value)
}
fun removeKeys(folder: String): Int? {
return context?.removeKeys(folder)
}
fun <T> setKey(path: String, value: T) {
context?.setKey(path, value)
}
fun <T> setKey(folder: String, path: String, value: T) {
context?.setKey(folder, path, value)
}
inline fun <reified T : Any> getKey(path: String, defVal: T?): T? {
return context?.getKey(path, defVal)
}
inline fun <reified T : Any> getKey(path: String): T? {
return context?.getKey(path)
}
inline fun <reified T : Any> getKey(folder: String, path: String): T? {
return context?.getKey(folder, path)
}
inline fun <reified T : Any> getKey(folder: String, path: String, defVal: T?): T? {
return context?.getKey(folder, path, defVal)
}
fun getKeys(folder: String): List<String>? {
return context?.getKeys(folder)
}
fun removeKey(folder: String, path: String) {
context?.removeKey(folder, path)
}
fun removeKey(path: String) {
context?.removeKey(path)
}
fun openBrowser(url: String) {
context?.openBrowser(url)
}
}
}

View File

@ -164,6 +164,25 @@ object APIHolder {
return realSet
}
fun Context.filterProviderByPreferredMedia(): List<MainAPI> {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val currentPrefMedia = settingsManager.getInt(this.getString(R.string.prefer_media_type_key), 0)
val langs = this.getApiProviderLangSettings()
val allApis = apis.filter { langs.contains(it.lang) }.filter { api -> api.hasMainPage }
return if (currentPrefMedia < 1) {
allApis
} else {
// Filter API depending on preferred media type
val listEnumAnime = listOf(TvType.Anime, TvType.AnimeMovie, TvType.ONA)
val listEnumMovieTv = listOf(TvType.Movie, TvType.TvSeries, TvType.Cartoon)
val mediaTypeList = if (currentPrefMedia == 1) listEnumMovieTv else listEnumAnime
val filteredAPI =
allApis.filter { api -> api.supportedTypes.any { it in mediaTypeList } }
filteredAPI
}
}
}
/**Every provider will **not** have try catch built in, so handle exceptions when calling these functions*/
@ -525,25 +544,27 @@ fun MainAPI.newMovieLoadResponse(
name: String,
url: String,
type: TvType,
dataUrl : String,
dataUrl: String,
initializer: MovieLoadResponse.() -> Unit = { }
): MovieLoadResponse {
val builder = MovieLoadResponse(name = name, url = url, apiName = this.name, type = type,dataUrl = dataUrl)
val builder = MovieLoadResponse(name = name, url = url, apiName = this.name, type = type, dataUrl = dataUrl)
builder.initializer()
return builder
}
fun LoadResponse.setDuration(input : String?) {
fun LoadResponse.setDuration(input: String?) {
if (input == null) return
Regex("([0-9]*)h.*?([0-9]*)m").matchEntire(input)?.groupValues?.let { values ->
if(values.size == 3) {
if (values.size == 3) {
val hours = values[1].toIntOrNull()
val minutes = values[2].toIntOrNull()
this.duration = if(minutes != null && hours != null) { hours * 60 + minutes } else null
this.duration = if (minutes != null && hours != null) {
hours * 60 + minutes
} else null
}
}
Regex("([0-9]*)m").matchEntire(input)?.groupValues?.let { values ->
if(values.size == 2) {
if (values.size == 2) {
this.duration = values[1].toIntOrNull()
}
}
@ -584,7 +605,7 @@ fun MainAPI.newTvSeriesLoadResponse(
name: String,
url: String,
type: TvType,
episodes : List<TvSeriesEpisode>,
episodes: List<TvSeriesEpisode>,
initializer: TvSeriesLoadResponse.() -> Unit = { }
): TvSeriesLoadResponse {
val builder = TvSeriesLoadResponse(name = name, url = url, apiName = this.name, type = type, episodes = episodes)

View File

@ -490,7 +490,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
api.handleRedirect(this, str)
api.handleRedirect(str)
}
}
} else {
@ -511,7 +511,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onCreate(savedInstanceState: Bundle?) {
// init accounts
for (api in OAuth2accountApis) {
api.init(this)
api.init()
}
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)

View File

@ -1,9 +1,8 @@
package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
abstract class AccountManager(private val defIndex: Int) : OAuth2API {
var accountIndex = defIndex
@ -13,44 +12,44 @@ abstract class AccountManager(private val defIndex: Int) : OAuth2API {
// int array of all accounts indexes
private val accountsKey get() = "${idPrefix}_accounts"
protected fun Context.removeAccountKeys() {
this.removeKeys(accountId)
val accounts = getAccounts(this).toMutableList()
protected fun removeAccountKeys() {
removeKeys(accountId)
val accounts = getAccounts()?.toMutableList() ?: mutableListOf()
accounts.remove(accountIndex)
this.setKey(accountsKey, accounts.toIntArray())
setKey(accountsKey, accounts.toIntArray())
init(this)
init()
}
fun getAccounts(context: Context): IntArray {
return context.getKey(accountsKey, intArrayOf())!!
fun getAccounts(): IntArray? {
return getKey(accountsKey, intArrayOf())
}
fun init(context: Context) {
accountIndex = context.getKey(accountActiveKey, defIndex)!!
val accounts = getAccounts(context)
if (accounts.isNotEmpty() && this.loginInfo(context) == null) {
fun init() {
accountIndex = getKey(accountActiveKey, defIndex)!!
val accounts = getAccounts()
if (accounts?.isNotEmpty() == true && this.loginInfo() == null) {
accountIndex = accounts.first()
}
}
protected fun Context.switchToNewAccount() {
val accounts = getAccounts(this)
accountIndex = (accounts.maxOrNull() ?: 0) + 1
protected fun switchToNewAccount() {
val accounts = getAccounts()
accountIndex = (accounts?.maxOrNull() ?: 0) + 1
}
protected fun Context.registerAccount() {
this.setKey(accountActiveKey, accountIndex)
val accounts = getAccounts(this).toMutableList()
protected fun registerAccount() {
setKey(accountActiveKey, accountIndex)
val accounts = getAccounts()?.toMutableList() ?: mutableListOf()
if (!accounts.contains(accountIndex)) {
accounts.add(accountIndex)
}
this.setKey(accountsKey, accounts.toIntArray())
setKey(accountsKey, accounts.toIntArray())
}
fun changeAccount(context: Context, index: Int) {
fun changeAccount(index: Int) {
accountIndex = index
context.setKey(accountActiveKey, index)
setKey(accountActiveKey, index)
}
}

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import java.util.concurrent.TimeUnit
@ -13,11 +12,11 @@ interface OAuth2API {
// don't change this as all keys depend on it
val idPrefix : String
fun handleRedirect(context: Context, url: String)
fun authenticate(context: Context)
fun handleRedirect(url: String)
fun authenticate()
fun loginInfo(context: Context): LoginInfo?
fun logOut(context: Context)
fun loginInfo(): LoginInfo?
fun logOut()
class LoginInfo(
val profilePicture: String?,

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import com.lagradost.cloudstream3.ShowStatus
interface SyncAPI : OAuth2API {
@ -67,7 +66,7 @@ interface SyncAPI : OAuth2API {
val icon: Int
val mainUrl: String
fun search(context: Context, name: String): List<SyncSearchResult>?
fun search(name: String): List<SyncSearchResult>?
/**
-1 -> None
@ -78,9 +77,9 @@ interface SyncAPI : OAuth2API {
4 -> PlanToWatch
5 -> ReWatching
*/
fun score(context: Context, id: String, status: SyncStatus): Boolean
fun score(id: String, status: SyncStatus): Boolean
fun getStatus(context: Context, id: String): SyncStatus?
fun getStatus(id: String): SyncStatus?
fun getResult(context: Context, id: String): SyncResult?
fun getResult(id: String): SyncResult?
}

View File

@ -1,11 +1,14 @@
package com.lagradost.cloudstream3.syncproviders.providers
import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
@ -15,12 +18,8 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.maxStale
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL
import java.util.*
@ -33,9 +32,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val mainUrl = "https://anilist.co"
override val icon = R.drawable.ic_anilist_icon
override fun loginInfo(context: Context): OAuth2API.LoginInfo? {
override fun loginInfo(): OAuth2API.LoginInfo? {
// context.getUser(true)?.
context.getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user ->
getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo(
profilePicture = user.picture,
name = user.name,
@ -45,16 +44,16 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
return null
}
override fun logOut(context: Context) {
context.removeAccountKeys()
override fun logOut() {
removeAccountKeys()
}
override fun authenticate(context: Context) {
override fun authenticate() {
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
context.openBrowser(request)
openBrowser(request)
}
override fun handleRedirect(context: Context, url: String) {
override fun handleRedirect(url: String) {
try {
val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
@ -63,19 +62,19 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val endTime = unixTime + expiresIn.toLong()
context.switchToNewAccount()
context.setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
context.setKey(accountId, ANILIST_TOKEN_KEY, token)
context.setKey(ANILIST_SHOULD_UPDATE_LIST, true)
switchToNewAccount()
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
setKey(accountId, ANILIST_TOKEN_KEY, token)
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
ioSafe {
context.getUser()
getUser()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun search(context: Context, name: String): List<SyncAPI.SyncSearchResult>? {
override fun search(name: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(name) ?: return null
return data.data.Page.media.map {
SyncAPI.SyncSearchResult(
@ -88,7 +87,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
override fun getResult(context: Context, id: String): SyncAPI.SyncResult? {
override fun getResult(id: String): SyncAPI.SyncResult? {
val internalId = id.toIntOrNull() ?: return null
val season = getSeason(internalId)?.data?.Media ?: return null
@ -104,9 +103,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
}
override fun getStatus(context: Context, id: String): SyncAPI.SyncStatus? {
override fun getStatus(id: String): SyncAPI.SyncStatus? {
val internalId = id.toIntOrNull() ?: return null
val data = context.getDataAboutId(internalId) ?: return null
val data = getDataAboutId(internalId) ?: return null
return SyncAPI.SyncStatus(
score = data.score,
@ -116,13 +115,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
}
override fun score(context: Context, id: String, status: SyncAPI.SyncStatus): Boolean {
return context.postDataAboutId(
override fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
return postDataAboutId(
id.toIntOrNull() ?: return false,
fromIntToAnimeStatus(status.status),
status.score,
status.watchedEpisodes
)
) ?: return false
}
companion object {
@ -331,21 +330,21 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
fun Context.initGetUser() {
fun initGetUser() {
if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return
ioSafe {
getUser()
}
}
private fun Context.checkToken(): Boolean {
private fun checkToken(): Boolean {
return unixTime > getKey(
accountId,
ANILIST_UNIXTIME_KEY, 0L
)!!
}
fun Context.getDataAboutId(id: Int): AniListTitleHolder? {
fun getDataAboutId(id: Int): AniListTitleHolder? {
val q =
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
@ -404,7 +403,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
private fun Context.postApi(url: String, q: String, cache: Boolean = false): String {
private fun postApi(url: String, q: String, cache: Boolean = false): String {
return try {
if (!checkToken()) {
// println("VARS_ " + vars)
@ -504,11 +503,11 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
)
fun Context.getAnilistListCached(): Array<Lists>? {
fun getAnilistListCached(): Array<Lists>? {
return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
}
fun Context.getAnilistAnimeListSmart(): Array<Lists>? {
fun getAnilistAnimeListSmart(): Array<Lists>? {
if (getKey<String>(
accountId,
ANILIST_TOKEN_KEY,
@ -529,11 +528,11 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
private fun Context.getFullAnilistList(): FullAnilistList? {
private fun getFullAnilistList(): FullAnilistList? {
try {
var userID: Int? = null
/** WARNING ASSUMES ONE USER! **/
getKeys(ANILIST_USER_KEY).forEach { key ->
getKeys(ANILIST_USER_KEY)?.forEach { key ->
getKey<AniListUser>(key, null)?.let {
userID = it.id
}
@ -591,7 +590,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
fun Context.toggleLike(id: Int): Boolean {
fun toggleLike(id: Int): Boolean {
val q = """mutation (${'$'}animeId: Int = $id) {
ToggleFavourite (animeId: ${'$'}animeId) {
anime {
@ -608,7 +607,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
return data != ""
}
private fun Context.postDataAboutId(id: Int, type: AniListStatusType, score: Int?, progress: Int?): Boolean {
private fun postDataAboutId(id: Int, type: AniListStatusType, score: Int?, progress: Int?): Boolean {
try {
val q =
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
@ -632,7 +631,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
private fun Context.getUser(setSettings: Boolean = true): AniListUser? {
private fun getUser(setSettings: Boolean = true): AniListUser? {
val q = """
{
Viewer {

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.syncproviders.providers
import android.content.Context
import com.lagradost.cloudstream3.syncproviders.OAuth2API
//TODO dropbox sync
@ -10,19 +9,19 @@ class Dropbox : OAuth2API {
override val key = "zlqsamadlwydvb2"
override val redirectUrl = "dropboxlogin"
override fun authenticate(context: Context) {
override fun authenticate() {
TODO("Not yet implemented")
}
override fun handleRedirect(context: Context,url: String) {
override fun handleRedirect(url: String) {
TODO("Not yet implemented")
}
override fun logOut(context: Context) {
override fun logOut() {
TODO("Not yet implemented")
}
override fun loginInfo(context: Context): OAuth2API.LoginInfo? {
override fun loginInfo(): OAuth2API.LoginInfo? {
TODO("Not yet implemented")
}
}

View File

@ -1,12 +1,14 @@
package com.lagradost.cloudstream3.syncproviders.providers
import android.content.Context
import android.util.Base64
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
@ -16,11 +18,8 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL
import java.security.SecureRandom
@ -40,26 +39,27 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val icon: Int
get() = R.drawable.mal_logo
override fun logOut(context: Context) {
context.removeAccountKeys()
override fun logOut() {
removeAccountKeys()
}
override fun loginInfo(context: Context): OAuth2API.LoginInfo? {
//context.getMalUser(true)?
context.getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
override fun loginInfo(): OAuth2API.LoginInfo? {
//getMalUser(true)?
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo(profilePicture = user.picture, name = user.name, accountIndex = accountIndex)
}
return null
}
override fun search(context: Context, name: String): List<SyncAPI.SyncSearchResult> {
override fun search(name: String): List<SyncAPI.SyncSearchResult> {
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
var res = app.get(
val auth = getKey<String>(
accountId,
MAL_TOKEN_KEY
) ?: return emptyList()
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer " + context.getKey<String>(
accountId,
MAL_TOKEN_KEY
)!!,
"Authorization" to "Bearer " + auth,
), cacheTime = 0
).text
return mapper.readValue<MalSearch>(res).data.map {
@ -74,8 +74,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
override fun score(context: Context, id: String, status : SyncAPI.SyncStatus): Boolean {
return context.setScoreRequest(
override fun score(id: String, status : SyncAPI.SyncStatus): Boolean {
return setScoreRequest(
id.toIntOrNull() ?: return false,
fromIntToAnimeStatus(status.status),
status.score,
@ -83,15 +83,15 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
)
}
override fun getResult(context: Context, id: String): SyncAPI.SyncResult? {
override fun getResult(id: String): SyncAPI.SyncResult? {
val internalId = id.toIntOrNull() ?: return null
TODO("Not yet implemented")
}
override fun getStatus(context: Context, id: String): SyncAPI.SyncStatus? {
override fun getStatus(id: String): SyncAPI.SyncStatus? {
val internalId = id.toIntOrNull() ?: return null
val data = context.getDataAboutMalId(internalId)?.my_list_status ?: return null
val data = getDataAboutMalId(internalId)?.my_list_status ?: return null
return SyncAPI.SyncStatus(
score = data.score,
status = malStatusAsString.indexOf(data.status),
@ -111,7 +111,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api
}
override fun handleRedirect(context: Context, url: String) {
override fun handleRedirect(url: String) {
try {
val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
@ -136,10 +136,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
if (res != "") {
context.switchToNewAccount()
context.storeToken(res)
context.getMalUser()
context.setKey(MAL_SHOULD_UPDATE_LIST, true)
switchToNewAccount()
storeToken(res)
getMalUser()
setKey(MAL_SHOULD_UPDATE_LIST, true)
}
}
}
@ -148,7 +148,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
override fun authenticate(context: Context) {
override fun authenticate() {
// It is recommended to use a URL-safe string as code_verifier.
// See section 4 of RFC 7636 for more details.
@ -161,7 +161,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val codeChallenge = codeVerifier
val request =
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
context.openBrowser(request)
openBrowser(request)
}
private val mapper = JsonMapper.builder().addModule(KotlinModule())
@ -170,7 +170,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private var requestId = 0
private var codeVerifier = ""
private fun Context.storeToken(response: String) {
private fun storeToken(response: String) {
try {
if (response != "") {
val token = mapper.readValue<ResponseToken>(response)
@ -183,7 +183,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
private fun Context.refreshToken() {
private fun refreshToken() {
try {
val res = app.post(
"https://myanimelist.net/v1/oauth2/token",
@ -278,11 +278,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("start_time") val start_time: String?
)
fun Context.getMalAnimeListCached(): Array<Data>? {
fun getMalAnimeListCached(): Array<Data>? {
return getKey(MAL_CACHED_LIST) as? Array<Data>
}
fun Context.getMalAnimeListSmart(): Array<Data>? {
fun getMalAnimeListSmart(): Array<Data>? {
if (getKey<String>(
accountId,
MAL_TOKEN_KEY
@ -300,7 +300,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
private fun Context.getMalAnimeList(): Array<Data>? {
private fun getMalAnimeList(): Array<Data>? {
return try {
checkMalToken()
var offset = 0
@ -322,8 +322,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
}
private fun Context.getMalAnimeListSlice(offset: Int = 0): MalList? {
private fun getMalAnimeListSlice(offset: Int = 0): MalList? {
val user = "@me"
val auth = getKey<String>(
accountId,
MAL_TOKEN_KEY
) ?: return null
return try {
// Very lackluster docs
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
@ -331,10 +335,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset"
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer " + getKey<String>(
accountId,
MAL_TOKEN_KEY
)!!,
"Authorization" to "Bearer $auth",
), cacheTime = 0
).text
res.toKotlinObject()
@ -344,7 +345,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
private fun Context.getDataAboutMalId(id: Int): MalAnime? {
private fun getDataAboutMalId(id: Int): MalAnime? {
return try {
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
val url = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
@ -362,7 +363,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
fun Context.setAllMalData() {
fun setAllMalData() {
val user = "@me"
var isDone = false
var index = 0
@ -426,7 +427,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return null
}
private fun Context.checkMalToken() {
private fun checkMalToken() {
if (unixTime > getKey(
accountId,
MAL_UNIXTIME_KEY
@ -436,7 +437,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
private fun Context.getMalUser(setSettings: Boolean = true): MalUser? {
private fun getMalUser(setSettings: Boolean = true): MalUser? {
checkMalToken()
return try {
val res = app.get(
@ -483,7 +484,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
fun Context.setScoreRequest(
fun setScoreRequest(
id: Int,
status: MalStatusType? = null,
score: Int? = null,
@ -514,7 +515,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
private fun Context.setScoreRequest(
private fun setScoreRequest(
id: Int,
status: String? = null,
score: Int? = null,

View File

@ -113,7 +113,7 @@ object DownloadButtonSetup {
if (click.data.episode <= 0) null else click.data.episode,
click.data.season
),
act.getViewPos(click.data.id)?.position ?: 0
getViewPos(click.data.id)?.position ?: 0
)
)
}

View File

@ -106,7 +106,7 @@ class DownloadChildAdapter(
localCard = card
val d = card.data
val posDur = itemView.context.getViewPos(d.id)
val posDur = getViewPos(d.id)
if (posDur != null) {
val visualPos = posDur.fixVisual()
progressBar.max = (visualPos.duration / 1000).toInt()

View File

@ -14,7 +14,6 @@ import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSnapHelper
@ -22,7 +21,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
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
@ -36,7 +38,6 @@ import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
@ -130,14 +131,12 @@ class HomeFragment : Fragment() {
}
private val apiChangeClickListener = View.OnClickListener { view ->
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val currentPrefMedia = settingsManager.getInt(getString(R.string.preferred_media_settings), 0)
val validAPIs = AppUtils.filterProviderByPreferredMedia(apis, currentPrefMedia).toMutableList()
val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf()
validAPIs.add(0, randomApi)
validAPIs.add(0, noneApi)
view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api -> Pair(index, api.name) }) {
homeViewModel.loadAndCancel(validAPIs[itemId].name, currentPrefMedia)
homeViewModel.loadAndCancel(validAPIs[itemId].name)
}
}
@ -157,14 +156,12 @@ class HomeFragment : Fragment() {
}*/
private fun reloadStored() {
context?.let { ctx ->
homeViewModel.loadResumeWatching(ctx)
val list = EnumSet.noneOf(WatchType::class.java)
ctx.getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
list.addAll(it)
}
homeViewModel.loadStoredData(ctx, list)
homeViewModel.loadResumeWatching()
val list = EnumSet.noneOf(WatchType::class.java)
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
list.addAll(it)
}
homeViewModel.loadStoredData(list)
}
/*private fun handleBack(poppedFragment: Boolean) {
@ -182,7 +179,7 @@ class HomeFragment : Fragment() {
home_change_api_loading.setOnClickListener(apiChangeClickListener)
observe(homeViewModel.apiName) { apiName ->
context?.setKey(HOMEPAGE_API, apiName)
setKey(HOMEPAGE_API, apiName)
home_provider_name?.text = apiName
home_provider_meta_info?.isVisible = false
@ -326,11 +323,11 @@ class HomeFragment : Fragment() {
} else {
list.add(watch)
}
homeViewModel.loadStoredData(itemView.context, list)
homeViewModel.loadStoredData(list)
}
item.first?.setOnLongClickListener { itemView ->
homeViewModel.loadStoredData(itemView.context, EnumSet.of(watch))
item.first?.setOnLongClickListener {
homeViewModel.loadStoredData(EnumSet.of(watch))
return@setOnLongClickListener true
}
}
@ -404,7 +401,7 @@ class HomeFragment : Fragment() {
if (id != null) {
callback.view.popupMenuNoIcons(listOf(Pair(0, R.string.action_remove_from_bookmarks))) {
if (itemId == 0) {
activity?.setResultWatchState(id, WatchType.NONE.internalId)
setResultWatchState(id, WatchType.NONE.internalId)
reloadStored()
}
}
@ -438,7 +435,7 @@ class HomeFragment : Fragment() {
if (itemId == 0) {
val card = callback.card
if (card is DataStoreHelper.ResumeWatchingResult) {
context?.removeLastWatched(card.parentId)
removeLastWatched(card.parentId)
reloadStored()
}
}
@ -476,11 +473,9 @@ class HomeFragment : Fragment() {
reloadStored()
val apiName = context?.getKey<String>(HOMEPAGE_API)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val currentPrefMedia = settingsManager.getInt(getString(R.string.preferred_media_settings), 0)
if (homeViewModel.apiName.value != apiName || apiName == null) {
//println("Caught home: " + homeViewModel.apiName.value + " at " + apiName)
homeViewModel.loadAndCancel(apiName, currentPrefMedia)
homeViewModel.loadAndCancel(apiName)
}
// nice profile pic on homepage
@ -497,7 +492,7 @@ class HomeFragment : Fragment() {
}
for (syncApi in OAuth2API.OAuth2Apis) {
val login = syncApi.loginInfo(ctx)
val login = syncApi.loginInfo()
val pic = login?.profilePicture
if (pic != null) {
home_profile_picture.setImage(pic)

View File

@ -1,13 +1,15 @@
package com.lagradost.cloudstream3.ui.home
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.HomePageResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.SearchResponse
@ -16,15 +18,16 @@ import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.HOMEPAGE_API
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -55,22 +58,22 @@ class HomeViewModel : ViewModel() {
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
fun loadResumeWatching(context: Context) = viewModelScope.launch {
fun loadResumeWatching() = viewModelScope.launch {
val resumeWatching = withContext(Dispatchers.IO) {
context.getAllResumeStateIds().mapNotNull { id ->
context.getLastWatched(id)
}.sortedBy { -it.updateTime }
getAllResumeStateIds()?.mapNotNull { id ->
getLastWatched(id)
}?.sortedBy { -it.updateTime }
}
// val resumeWatchingResult = ArrayList<DataStoreHelper.ResumeWatchingResult>()
val resumeWatchingResult = withContext(Dispatchers.IO) {
resumeWatching.map { resume ->
val data = context.getKey<VideoDownloadHelper.DownloadHeaderCached>(
resumeWatching?.map { resume ->
val data = getKey<VideoDownloadHelper.DownloadHeaderCached>(
DOWNLOAD_HEADER_CACHE,
resume.parentId.toString()
) ?: return@map null
val watchPos = context.getViewPos(resume.episodeId)
val watchPos = getViewPos(resume.episodeId)
DataStoreHelper.ResumeWatchingResult(
data.name,
data.url,
@ -84,18 +87,19 @@ class HomeViewModel : ViewModel() {
resume.season,
resume.isFromDownload
)
}.filterNotNull()
}?.filterNotNull()
}
_resumeWatching.postValue(resumeWatchingResult)
}
fun loadStoredData(context: Context, preferredWatchStatus: EnumSet<WatchType>?) = viewModelScope.launch {
fun loadStoredData(preferredWatchStatus: EnumSet<WatchType>?) = viewModelScope.launch {
val watchStatusIds = withContext(Dispatchers.IO) {
context.getAllWatchStateIds().map { id ->
Pair(id, context.getResultWatchState(id))
getAllWatchStateIds()?.map { id ->
Pair(id, getResultWatchState(id))
}
}.distinctBy { it.first }
}?.distinctBy { it.first } ?: return@launch
val length = WatchType.values().size
val currentWatchTypes = EnumSet.noneOf(WatchType::class.java)
@ -125,10 +129,10 @@ class HomeViewModel : ViewModel() {
val list = withContext(Dispatchers.IO) {
watchStatusIds.filter { watchPrefNotNull.contains(it.second) }
.mapNotNull { context.getBookmarkedData(it.first) }
.mapNotNull { getBookmarkedData(it.first) }
.sortedBy { -it.latestUpdatedTime }
}
_bookmarks.postValue(Pair(true,list))
_bookmarks.postValue(Pair(true, list))
}
private var onGoingLoad: Job? = null
@ -176,15 +180,19 @@ class HomeViewModel : ViewModel() {
}
}
fun loadAndCancel(preferredApiName: String?, currentPrefMedia: Int) = viewModelScope.launch {
fun loadAndCancel(preferredApiName: String?) = viewModelScope.launch {
val api = getApiFromNameNull(preferredApiName)
if (preferredApiName == noneApi.name)
loadAndCancel(noneApi)
else if (preferredApiName == randomApi.name || api == null) {
val validAPIs = AppUtils.filterProviderByPreferredMedia(apis, currentPrefMedia)
val apiRandom = validAPIs.random()
loadAndCancel(apiRandom)
context?.setKey(HOMEPAGE_API, apiRandom.name)
val validAPIs = context?.filterProviderByPreferredMedia()
if(validAPIs.isNullOrEmpty()) {
loadAndCancel(noneApi)
} else {
val apiRandom = validAPIs.random()
loadAndCancel(apiRandom)
setKey(HOMEPAGE_API, apiRandom.name)
}
} else {
loadAndCancel(api)
}

View File

@ -818,7 +818,6 @@ class PlayerFragment : Fragment() {
context?.let { ctx ->
//if (this::viewModel.isInitialized) {
viewModel.setViewPos(
ctx,
if (isDownloadedFile) uriData.id else getEpisode()?.id,
exoPlayer.currentPosition,
exoPlayer.duration
@ -832,7 +831,7 @@ class PlayerFragment : Fragment() {
}*/
if (isDownloadedFile) {
ctx.setLastWatched(
setLastWatched(
uriData.parentId,
uriData.id,
uriData.episode,
@ -840,7 +839,7 @@ class PlayerFragment : Fragment() {
true
)
} else
viewModel.reloadEpisodes(ctx)
viewModel.reloadEpisodes()
}
}
}
@ -1390,9 +1389,7 @@ class PlayerFragment : Fragment() {
handlePlayerEvent(PlayerEventType.Play)
}
context?.let { ctx ->
setPreferredSubLanguage(ctx.getAutoSelectLanguageISO639_1())
}
setPreferredSubLanguage(getAutoSelectLanguageISO639_1())
subView = player_view?.findViewById(R.id.exo_subtitles)
subView?.let { sView ->
@ -1400,7 +1397,7 @@ class PlayerFragment : Fragment() {
subtitle_holder.addView(sView)
}
subStyle = context?.getCurrentSavedStyle()!!
subStyle = getCurrentSavedStyle()
onSubStyleChanged(subStyle)
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged

View File

@ -38,7 +38,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
})
}
fun pushSync(activity: Activity?, autoSearch: String? = null, callback : (SearchClickCallback) -> Unit) {
fun pushSync(activity: Activity?, autoSearch: String? = null, callback: (SearchClickCallback) -> Unit) {
clickCallback = callback
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
putBoolean("mainapi", false)
@ -46,7 +46,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
})
}
var clickCallback : ((SearchClickCallback) -> Unit)? = null
var clickCallback: ((SearchClickCallback) -> Unit)? = null
}
private val searchViewModel: SearchViewModel by activityViewModels()
@ -124,10 +124,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
searchMagIcon.scaleY = 0.65f
quick_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
context?.let { ctx ->
searchViewModel.searchAndCancel(query = query, context = ctx, isMainApis = isMainApis, ignoreSettings = true)
}
searchViewModel.searchAndCancel(query = query, isMainApis = isMainApis, ignoreSettings = true)
quick_search?.let {
UIHelper.hideKeyboard(it)
}

View File

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Context.CLIPBOARD_SERVICE
import android.content.Intent
import android.content.Intent.*
@ -60,6 +59,7 @@ import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
@ -128,7 +128,7 @@ fun ResultEpisode.getDisplayPosition(): Long {
return position
}
fun Context.buildResultEpisode(
fun buildResultEpisode(
name: String?,
poster: String?,
episode: Int,
@ -269,7 +269,7 @@ class ResultFragment : Fragment() {
private var startValue: Int? = null
private fun updateSync(id: Int) {
val syncList = context?.getSync(id, SyncApis.map { it.idPrefix }) ?: return
val syncList = getSync(id, SyncApis.map { it.idPrefix }) ?: return
val list = ArrayList<Pair<SyncAPI, String>>()
for (i in 0 until SyncApis.count()) {
val res = syncList[i] ?: continue
@ -596,7 +596,7 @@ class ResultFragment : Fragment() {
// 1. Checks if the lang should be downloaded
// 2. Makes it into the download format
// 3. Downloads it as a .vtt file
val downloadList = ctx.getDownloadSubsLanguageISO639_1()
val downloadList = getDownloadSubsLanguageISO639_1()
main {
subs?.let { subsList ->
subsList.filter {
@ -857,7 +857,7 @@ class ResultFragment : Fragment() {
//.map { watchType -> Triple(watchType.internalId, watchType.iconRes, watchType.stringRes) },
) {
context?.let { localContext ->
viewModel.updateWatchStatus(localContext, WatchType.fromInternalId(this.itemId))
viewModel.updateWatchStatus(WatchType.fromInternalId(this.itemId))
}
}
}
@ -883,7 +883,7 @@ class ResultFragment : Fragment() {
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(fab.context, WatchType.values()[it])
viewModel.updateWatchStatus(WatchType.values()[it])
}
}
}
@ -938,9 +938,8 @@ class ResultFragment : Fragment() {
.map { Pair(it ?: -2, fromIndexToSeasonText(it)) },
) {
val id = this.itemId
context?.let {
viewModel.changeSeason(it, if (id == -2) null else id)
}
viewModel.changeSeason(if (id == -2) null else id)
}
}
}
@ -981,7 +980,7 @@ class ResultFragment : Fragment() {
if (ranges != null) {
it.popupMenuNoIconsAndNoStringRes(ranges.map { status -> Pair(status.ordinal, status.toString()) }
.toList()) {
viewModel.changeDubStatus(requireContext(), DubStatus.values()[itemId])
viewModel.changeDubStatus(DubStatus.values()[itemId])
}
}
}
@ -999,7 +998,7 @@ class ResultFragment : Fragment() {
val ranges = episodeRanges
if (ranges != null) {
it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) }.toList()) {
viewModel.changeRange(requireContext(), itemId)
viewModel.changeRange(itemId)
}
}
}
@ -1087,13 +1086,14 @@ class ResultFragment : Fragment() {
updateSync(d.getId())
result_add_sync?.setOnClickListener {
QuickSearchFragment.pushSync(activity, d.name) { click ->
context?.addSync(d.getId(), click.card.apiName, click.card.url)?.let {
showToast(
activity,
context?.getString(R.string.added_sync_format)?.format(click.card.name),
Toast.LENGTH_SHORT
)
}
addSync(d.getId(), click.card.apiName, click.card.url)
showToast(
activity,
context?.getString(R.string.added_sync_format)?.format(click.card.name),
Toast.LENGTH_SHORT
)
updateSync(d.getId())
}
}
@ -1290,7 +1290,7 @@ class ResultFragment : Fragment() {
val tempUrl = url
if (tempUrl != null) {
result_reload_connectionerror.setOnClickListener {
viewModel.load(it.context, tempUrl, apiName, showFillers)
viewModel.load(tempUrl, apiName, showFillers)
}
result_reload_connection_open_in_browser?.setOnClickListener {
@ -1304,18 +1304,12 @@ class ResultFragment : Fragment() {
}
result_meta_site?.setOnClickListener {
val i = Intent(ACTION_VIEW)
i.data = Uri.parse(tempUrl)
try {
startActivity(i)
} catch (e: Exception) {
e.printStackTrace()
}
it.context?.openBrowser(tempUrl)
}
if (restart || viewModel.resultResponse.value == null) {
viewModel.clear()
viewModel.load(ctx, tempUrl, apiName, showFillers)
viewModel.load(tempUrl, apiName, showFillers)
}
}
}

View File

@ -8,23 +8,24 @@ import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.getFillerEpisodes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -49,6 +50,7 @@ class ResultViewModel : ViewModel() {
id.value = null
selectedSeason.value = -2
_dubSubEpisodes.value = null
_sync.value = null
}
private var repo: APIRepository? = null
@ -89,17 +91,17 @@ class ResultViewModel : ViewModel() {
private val _sync: MutableLiveData<List<Resource<SyncAPI.SyncResult?>>> = MutableLiveData()
val sync: LiveData<List<Resource<SyncAPI.SyncResult?>>> get() = _sync
fun updateWatchStatus(context: Context, status: WatchType) = viewModelScope.launch {
fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
val currentId = id.value ?: return@launch
_watchStatus.postValue(status)
val resultPage = page.value
withContext(Dispatchers.IO) {
context.setResultWatchState(currentId, status.internalId)
setResultWatchState(currentId, status.internalId)
if (resultPage != null) {
val current = context.getBookmarkedData(currentId)
val current = getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis()
context.setBookmarkedData(
setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
currentId,
@ -117,13 +119,13 @@ class ResultViewModel : ViewModel() {
}
}
private fun loadWatchStatus(context: Context, localId: Int? = null) {
private fun loadWatchStatus(localId: Int? = null) {
val currentId = localId ?: id.value ?: return
val currentWatch = context.getResultWatchState(currentId)
val currentWatch = getResultWatchState(currentId)
_watchStatus.postValue(currentWatch)
}
private fun filterEpisodes(context: Context, list: List<ResultEpisode>?, selection: Int?, range: Int?) {
private fun filterEpisodes(list: List<ResultEpisode>?, selection: Int?, range: Int?) {
if (list == null) return
val seasonTypes = HashMap<Int?, Boolean>()
for (i in list) {
@ -141,7 +143,7 @@ class ResultViewModel : ViewModel() {
val realSelection = if (!seasonTypes.containsKey(selection)) seasons.first() else selection
val internalId = id.value
if (internalId != null) context.setResultSeason(internalId, realSelection)
if (internalId != null) setResultSeason(internalId, realSelection)
selectedSeason.postValue(realSelection ?: -2)
@ -187,33 +189,38 @@ class ResultViewModel : ViewModel() {
_publicEpisodes.postValue(Resource.Success(currentList))
}
fun changeSeason(context: Context, selection: Int?) {
filterEpisodes(context, _episodes.value, selection, null)
fun changeSeason(selection: Int?) {
filterEpisodes(_episodes.value, selection, null)
}
fun changeRange(context: Context, range: Int?) {
filterEpisodes(context, _episodes.value, null, range)
fun changeRange(range: Int?) {
filterEpisodes(_episodes.value, null, range)
}
fun changeDubStatus(context: Context, status: DubStatus?) {
fun changeDubStatus(status: DubStatus?) {
dubSubEpisodes.value?.get(status)?.let { episodes ->
id.value?.let {
if (status != null) {
setDub(it, status)
}
}
_dubStatus.postValue(status)
updateEpisodes(context, null, episodes, null)
updateEpisodes(null, episodes, null)
}
}
fun updateSync(context: Context?, sync: List<Pair<SyncAPI, String>>) = viewModelScope.launch {
if(context == null) return@launch
if (context == null) return@launch
val list = ArrayList<Resource<SyncAPI.SyncResult?>>()
for (s in sync) {
val result = safeApiCall { s.first.getResult(context, s.second) }
val result = safeApiCall { s.first.getResult(s.second) }
list.add(result)
_sync.postValue(list)
}
}
private fun updateEpisodes(context: Context, localId: Int?, list: List<ResultEpisode>, selection: Int?) {
private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) {
_episodes.postValue(list)
val set = HashMap<Int, Int>()
@ -221,25 +228,23 @@ class ResultViewModel : ViewModel() {
episodeById.postValue(set)
filterEpisodes(
context,
list,
if (selection == -1) context.getResultSeason(localId ?: id.value ?: return) else selection, null
if (selection == -1) getResultSeason(localId ?: id.value ?: return) else selection, null
)
}
fun reloadEpisodes(context: Context) {
fun reloadEpisodes() {
val current = _episodes.value ?: return
val copy = current.map {
val posDur = context.getViewPos(it.id)
val posDur = getViewPos(it.id)
it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0)
}
updateEpisodes(context, null, copy, selectedSeason.value)
updateEpisodes(null, copy, selectedSeason.value)
}
fun setViewPos(context: Context?, episodeId: Int?, pos: Long, dur: Long) {
fun setViewPos(episodeId: Int?, pos: Long, dur: Long) {
try {
if (context == null || episodeId == null) return
context.setViewPos(episodeId, pos, dur)
DataStoreHelper.setViewPos(episodeId, pos, dur)
var index = episodeById.value?.get(episodeId) ?: return
var startPos = pos
@ -251,7 +256,7 @@ class ResultViewModel : ViewModel() {
if (startDur > 0L && (startPos * 100 / startDur) > 95) {
index++
if (episodeList.size <= index) { // last episode
context.removeLastWatched(parentId)
removeLastWatched(parentId)
return
}
episode = episodeList[index]
@ -261,7 +266,7 @@ class ResultViewModel : ViewModel() {
continue
} else {
context.setLastWatched(parentId, episode.id, episode.episode, episode.season)
setLastWatched(parentId, episode.id, episode.episode, episode.season)
return
}
}
@ -279,7 +284,7 @@ class ResultViewModel : ViewModel() {
return name
}
fun load(context: Context, url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
_resultResponse.postValue(Resource.Loading(url))
_publicEpisodes.postValue(Resource.Loading())
@ -301,9 +306,9 @@ class ResultViewModel : ViewModel() {
page.postValue(d)
val mainId = d.getId()
id.postValue(mainId)
loadWatchStatus(context, mainId)
loadWatchStatus(mainId)
context.setKey(
setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
@ -319,11 +324,14 @@ class ResultViewModel : ViewModel() {
when (d) {
is AnimeLoadResponse -> {
//TODO context.getKey<>() isdub
if (d.episodes.isEmpty()) {
_dubSubEpisodes.postValue(emptyMap())
return@launch
}
val isDub =
d.episodes.containsKey(DubStatus.Dubbed) && !d.episodes[DubStatus.Dubbed].isNullOrEmpty()
val dubStatus = if (isDub) DubStatus.Dubbed else DubStatus.Subbed
val status = getDub(mainId)
val statuses = d.episodes.map { it.key }
val dubStatus = if (statuses.contains(status)) status else statuses.first()
_dubStatus.postValue(dubStatus)
_dubSubSelections.postValue(d.episodes.keys)
@ -335,23 +343,21 @@ class ResultViewModel : ViewModel() {
for ((index, i) in ep.value.withIndex()) {
val episode = i.episode ?: (index + 1)
episodes.add(
context.buildResultEpisode(
filterName(i.name),
i.posterUrl,
episode,
null, // TODO FIX SEASON
i.url,
apiName,
mainId + index + 1 + idIndex * 100000,
index,
i.rating,
i.description,
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
it.contains(episode) && it[episode] == true
} ?: false else false,
)
)
episodes.add(buildResultEpisode(
filterName(i.name),
i.posterUrl,
episode,
null, // TODO FIX SEASON
i.url,
apiName,
mainId + index + 1 + idIndex * 100000,
index,
i.rating,
i.description,
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
it.contains(episode) && it[episode] == true
} ?: false else false,
))
}
idIndex++
@ -360,7 +366,7 @@ class ResultViewModel : ViewModel() {
_dubSubEpisodes.postValue(res)
res[dubStatus]?.let { episodes ->
updateEpisodes(context, mainId, episodes, -1)
updateEpisodes(mainId, episodes, -1)
}
}
@ -368,7 +374,7 @@ class ResultViewModel : ViewModel() {
val episodes = ArrayList<ResultEpisode>()
for ((index, i) in d.episodes.withIndex()) {
episodes.add(
context.buildResultEpisode(
buildResultEpisode(
filterName(i.name),
i.posterUrl,
i.episode ?: (index + 1),
@ -382,32 +388,31 @@ class ResultViewModel : ViewModel() {
null,
)
)
}
updateEpisodes(context, mainId, episodes, -1)
updateEpisodes(mainId, episodes, -1)
}
is MovieLoadResponse -> {
updateEpisodes(
context, mainId, arrayListOf(
context.buildResultEpisode(
d.name,
null,
0,
null,
d.dataUrl,
d.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
)
), -1
)
buildResultEpisode(
d.name,
null,
0,
null,
d.dataUrl,
d.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
).let {
updateEpisodes(mainId, listOf(it), -1)
}
}
is TorrentLoadResponse -> {
updateEpisodes(
context, mainId, arrayListOf(
context.buildResultEpisode(
mainId, listOf(
buildResultEpisode(
d.name,
null,
0,

View File

@ -48,7 +48,9 @@ class SearchFragment : Fragment() {
fun List<SearchResponse>.filterSearchResponse(): List<SearchResponse> {
return this.filter { response ->
if (response is AnimeSearchResponse) {
(response.dubStatus.isNullOrEmpty()) || (response.dubStatus.any { APIRepository.dubStatusActive.contains(it) })
(response.dubStatus.isNullOrEmpty()) || (response.dubStatus.any {
APIRepository.dubStatusActive.contains(it)
})
} else {
true
}
@ -291,16 +293,14 @@ class SearchFragment : Fragment() {
}
}
if(context?.isTvSettings() == true) {
if (context?.isTvSettings() == true) {
search_filter.isFocusable = true
search_filter.isFocusableInTouchMode = true
}
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
context?.let { ctx ->
searchViewModel.searchAndCancel(query = query, context = ctx)
}
searchViewModel.searchAndCancel(query = query)
main_search?.let {
hideKeyboard(it)

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.ui.search
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@ -41,9 +40,9 @@ class SearchViewModel : ViewModel() {
}
var onGoingSearch: Job? = null
fun searchAndCancel(query: String, isMainApis : Boolean = true, ignoreSettings : Boolean = false, context: Context) {
fun searchAndCancel(query: String, isMainApis: Boolean = true, ignoreSettings: Boolean = false) {
onGoingSearch?.cancel()
onGoingSearch = search(query, isMainApis, ignoreSettings, context)
onGoingSearch = search(query, isMainApis, ignoreSettings)
}
data class SyncSearchResultSearchResponse(
@ -66,57 +65,59 @@ class SearchViewModel : ViewModel() {
)
}
private fun search(query: String, isMainApis : Boolean = true, ignoreSettings : Boolean = false, context: Context) = viewModelScope.launch {
if (query.length <= 1) {
clearSearch()
return@launch
}
private fun search(query: String, isMainApis: Boolean = true, ignoreSettings: Boolean = false) =
viewModelScope.launch {
if (query.length <= 1) {
clearSearch()
return@launch
}
_searchResponse.postValue(Resource.Loading())
_searchResponse.postValue(Resource.Loading())
val currentList = ArrayList<OnGoingSearch>()
val currentList = ArrayList<OnGoingSearch>()
_currentSearch.postValue(ArrayList())
_currentSearch.postValue(ArrayList())
withContext(Dispatchers.IO) { // This interrupts UI otherwise
if (isMainApis) {
repos.filter { a ->
ignoreSettings || (providersActive.size == 0 || providersActive.contains(a.name))
}.apmap { a -> // Parallel
val search = a.search(query)
currentList.add(OnGoingSearch(a.name, search))
_currentSearch.postValue(currentList)
}
} else {
syncApis.apmap { a ->
val search = safeApiCall {
a.search(context, query)?.map { it.toSearchResponse() } ?: throw ErrorLoadingException()
withContext(Dispatchers.IO) { // This interrupts UI otherwise
if (isMainApis) {
repos.filter { a ->
ignoreSettings || (providersActive.size == 0 || providersActive.contains(a.name))
}.apmap { a -> // Parallel
val search = a.search(query)
currentList.add(OnGoingSearch(a.name, search))
_currentSearch.postValue(currentList)
}
} else {
syncApis.apmap { a ->
val search = safeApiCall {
a.search(query)?.map { it.toSearchResponse() }
?: throw ErrorLoadingException()
}
currentList.add(OnGoingSearch(a.name, search))
currentList.add(OnGoingSearch(a.name, search))
}
}
}
}
_currentSearch.postValue(currentList)
_currentSearch.postValue(currentList)
val list = ArrayList<SearchResponse>()
val nestedList =
currentList.map { it.data }.filterIsInstance<Resource.Success<List<SearchResponse>>>().map { it.value }
val list = ArrayList<SearchResponse>()
val nestedList =
currentList.map { it.data }.filterIsInstance<Resource.Success<List<SearchResponse>>>().map { it.value }
// I do it this way to move the relevant search results to the top
var index = 0
while (true) {
var added = 0
for (sublist in nestedList) {
if (sublist.size > index) {
list.add(sublist[index])
added++
// I do it this way to move the relevant search results to the top
var index = 0
while (true) {
var added = 0
for (sublist in nestedList) {
if (sublist.size > index) {
list.add(sublist[index])
added++
}
}
if (added == 0) break
index++
}
if (added == 0) break
index++
}
_searchResponse.postValue(Resource.Success(list))
}
_searchResponse.postValue(Resource.Success(list))
}
}

View File

@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.widget.ImageView
@ -24,6 +25,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.getApiSettings
import com.lagradost.cloudstream3.APIHolder.restrictedApis
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.MainActivity.Companion.setLocale
import com.lagradost.cloudstream3.MainActivity.Companion.showToast
@ -36,8 +38,6 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.HOMEPAGE_API
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.Qualities
@ -50,7 +50,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadDir
import com.lagradost.cloudstream3.utils.VideoDownloadManager.isScopedStorage
import java.io.File
import kotlin.concurrent.thread
@ -126,13 +125,14 @@ class SettingsFragment : PreferenceFragmentCompat() {
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top
private fun showAccountSwitch(context: Context, api: AccountManager) {
val accounts = api.getAccounts() ?: return
val builder =
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch)
val dialog = builder.show()
val accounts = api.getAccounts(context)
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener {
api.authenticate(it.context)
api.authenticate()
}
val ogIndex = api.accountIndex
@ -141,7 +141,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
for (index in accounts) {
api.accountIndex = index
val accountInfo = api.loginInfo(context)
val accountInfo = api.loginInfo()
if (accountInfo != null) {
items.add(accountInfo)
}
@ -149,26 +149,26 @@ class SettingsFragment : PreferenceFragmentCompat() {
api.accountIndex = ogIndex
val adapter = AccountAdapter(items, R.layout.account_single) {
dialog?.dismissSafe(activity)
api.changeAccount(it.view.context, it.card.accountIndex)
api.changeAccount(it.card.accountIndex)
}
val list = dialog.findViewById<RecyclerView>(R.id.account_list)
list?.adapter = adapter
}
private fun showLoginInfo(context: Context, api: AccountManager, info: OAuth2API.LoginInfo) {
private fun showLoginInfo(api: AccountManager, info: OAuth2API.LoginInfo) {
val builder =
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_managment)
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 {
it.context?.let { ctx ->
api.logOut(ctx)
dialog.dismissSafe(activity)
}
api.logOut()
dialog.dismissSafe(activity)
}
dialog.findViewById<TextView>(R.id.account_name)?.text = info.name ?: context.getString(R.string.no_data)
(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.dismissSafe(activity)
@ -208,11 +208,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
title = getString(R.string.login_format).format(api.name, getString(R.string.account))
setOnPreferenceClickListener { pref ->
pref.context?.let { ctx ->
val info = api.loginInfo(ctx)
val info = api.loginInfo()
if (info != null) {
showLoginInfo(ctx, api, info)
showLoginInfo(api, info)
} else {
api.authenticate(ctx)
api.authenticate()
}
}
@ -305,7 +305,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
// app_name_download_path = Cloudstream and does not change depending on release.
// DOES NOT WORK ON SCOPED STORAGE.
val secondaryDir = if (isScopedStorage) null else Environment.getExternalStorageDirectory().absolutePath +
val secondaryDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath +
File.separator + resources.getString(R.string.app_name_download_path)
val first = listOf(defaultDir, secondaryDir)
return (try {
@ -352,7 +352,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val currentPrefMedia =
settingsManager.getInt(getString(R.string.preferred_media_settings), 0)
settingsManager.getInt(getString(R.string.prefer_media_type_key), 0)
activity?.showBottomDialog(
prefNames.toList(),
@ -361,15 +361,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
true,
{}) {
settingsManager.edit()
.putInt(getString(R.string.preferred_media_settings), prefValues[it])
.putInt(getString(R.string.prefer_media_type_key), prefValues[it])
.apply()
val apilist = AppUtils.filterProviderByPreferredMedia(apis, prefValues[it])
val apiRandom = if (apilist.size > 0) {
apilist.random().name
} else {
""
}
context?.setKey(HOMEPAGE_API, apiRandom)
removeKey(HOMEPAGE_API)
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
}
return@setOnPreferenceClickListener true

View File

@ -19,10 +19,11 @@ import androidx.fragment.app.Fragment
import com.google.android.exoplayer2.text.Cue
import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.jaredrummler.android.colorpicker.ColorPickerDialog
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.MainActivity.Companion.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -87,8 +88,8 @@ class SubtitlesFragment : Fragment() {
this.setKey(SUBTITLE_KEY, style)
}
fun Context.getCurrentSavedStyle(): SaveCaptionStyle {
return this.getKey(SUBTITLE_KEY) ?: SaveCaptionStyle(
fun getCurrentSavedStyle(): SaveCaptionStyle {
return getKey(SUBTITLE_KEY) ?: SaveCaptionStyle(
getDefColor(0),
getDefColor(2),
getDefColor(3),
@ -109,11 +110,11 @@ class SubtitlesFragment : Fragment() {
return TypedValue.applyDimension(unit, size, metrics).toInt()
}
fun Context.getDownloadSubsLanguageISO639_1(): List<String> {
fun getDownloadSubsLanguageISO639_1(): List<String> {
return getKey(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en")
}
fun Context.getAutoSelectLanguageISO639_1(): String {
fun getAutoSelectLanguageISO639_1(): String {
return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en"
}
}
@ -184,7 +185,7 @@ class SubtitlesFragment : Fragment() {
context?.fixPaddingStatusbar(subs_root)
state = requireContext().getCurrentSavedStyle()
state = getCurrentSavedStyle()
context?.updateState()
fun View.setup(id: Int) {
@ -381,17 +382,17 @@ class SubtitlesFragment : Fragment() {
val lang639_1 = langMap.map { it.ISO_639_1 }
activity?.showDialog(
langMap.map { it.languageName },
lang639_1.indexOf(textView.context.getAutoSelectLanguageISO639_1()),
lang639_1.indexOf(getAutoSelectLanguageISO639_1()),
(textView as TextView).text.toString(),
true,
dismissCallback
) { index ->
textView.context.setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
}
}
subs_auto_select_language.setOnLongClickListener { textView ->
textView.context.setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
subs_auto_select_language.setOnLongClickListener {
setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
@ -399,7 +400,7 @@ class SubtitlesFragment : Fragment() {
subs_download_languages.setOnClickListener { textView ->
val langMap = SubtitleHelper.languages
val lang639_1 = langMap.map { it.ISO_639_1 }
val keys = textView.context.getDownloadSubsLanguageISO639_1()
val keys = getDownloadSubsLanguageISO639_1()
val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 }
activity?.showMultiDialog(
@ -408,12 +409,12 @@ class SubtitlesFragment : Fragment() {
(textView as TextView).text.toString(),
dismissCallback
) { indexList ->
textView.context.setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList())
setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList())
}
}
subs_download_languages.setOnLongClickListener { textView ->
textView.context.setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
subs_download_languages.setOnLongClickListener {
setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true

View File

@ -21,6 +21,7 @@ import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.wrappers.Wrappers
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import java.net.URL
@ -46,17 +47,21 @@ object AppUtils {
}
fun Context.openBrowser(url: String) {
val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java))
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
try {
val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java))
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
startActivity(
Intent.createChooser(intent, null)
.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components)
)
else
startActivity(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
startActivity(
Intent.createChooser(intent, null)
.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components)
)
else
startActivity(intent)
} catch (e : Exception) {
logError(e)
}
}
fun splitQuery(url: URL): Map<String, String> {
@ -233,23 +238,4 @@ object AppUtils {
}
return currentAudioFocusRequest
}
fun filterProviderByPreferredMedia(
apis: ArrayList<MainAPI>,
currentPrefMedia: Int
): List<MainAPI> {
val allApis = apis.filter { api -> api.hasMainPage }
return if (currentPrefMedia < 1) {
allApis
} else {
// Filter API depending on preferred media type
val listEnumAnime = listOf(TvType.Anime, TvType.AnimeMovie, TvType.ONA)
val listEnumMovieTv = listOf(TvType.Movie, TvType.TvSeries, TvType.Cartoon)
val mediaTypeList = if (currentPrefMedia == 1) listEnumMovieTv else listEnumAnime
val filteredAPI =
allApis.filter { api -> api.supportedTypes.any { it in mediaTypeList } }
filteredAPI
}
}
}

View File

@ -40,7 +40,9 @@ object CastHelper {
(epData.name ?: "Episode ${epData.episode}") + " - ${link.name}"
)
movieMetadata.putString(MediaMetadata.KEY_TITLE, holder.title)
holder.title?.let {
movieMetadata.putString(MediaMetadata.KEY_TITLE, it)
}
val srcPoster = epData.poster ?: holder.poster
if (srcPoster != null) {
@ -58,7 +60,7 @@ object CastHelper {
val builder = MediaInfo.Builder(link.url)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(MimeTypes.VIDEO_UNKNOWN)
.setContentType(if (link.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.VIDEO_MP4)
.setMetadata(movieMetadata)
.setMediaTracks(tracks)
data?.let {
@ -95,8 +97,8 @@ object CastHelper {
subtitles: List<SubtitleFile>,
startIndex: Int? = null,
startTime: Long? = null,
) : Boolean {
if(this == null) return false
): Boolean {
if (this == null) return false
if (episodes.isEmpty()) return false
if (currentLinks.size <= currentEpisodeIndex) return false
@ -105,13 +107,15 @@ object CastHelper {
val holder =
MetadataHolder(apiName, isMovie, title, poster, currentEpisodeIndex, episodes, currentLinks, subtitles)
val index = if(startIndex == null || startIndex < 0) 0 else startIndex
val index = if (startIndex == null || startIndex < 0) 0 else startIndex
val mediaItem =
getMediaInfo(epData, holder, index, JSONObject(mapper.writeValueAsString(holder)), subtitles)
awaitLinks(
this.remoteMediaClient?.load(MediaLoadRequestData.Builder().setMediaInfo(mediaItem).setCurrentTime(startTime ?: 0L).build() )
this.remoteMediaClient?.load(
MediaLoadRequestData.Builder().setMediaInfo(mediaItem).setCurrentTime(startTime ?: 0L).build()
)
) {
if (currentLinks.size > index + 1)
startCast(

View File

@ -1,19 +1,20 @@
package com.lagradost.cloudstream3.utils
import android.content.Context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
const val VIDEO_POS_DUR = "video_pos_dur"
const val RESULT_WATCH_STATE = "result_watch_state"
const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
const val RESULT_RESUME_WATCHING = "result_resume_watching"
const val RESULT_SEASON = "result_season"
const val RESULT_DUB = "result_dub"
object DataStoreHelper {
data class PosDur(val position: Long, val duration: Long)
@ -57,21 +58,21 @@ object DataStoreHelper {
var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
fun Context.getAllWatchStateIds(): List<Int> {
fun getAllWatchStateIds(): List<Int>? {
val folder = "$currentAccount/$RESULT_WATCH_STATE"
return getKeys(folder).mapNotNull {
return getKeys(folder)?.mapNotNull {
it.removePrefix("$folder/").toIntOrNull()
}
}
fun Context.getAllResumeStateIds(): List<Int> {
fun getAllResumeStateIds(): List<Int>? {
val folder = "$currentAccount/$RESULT_RESUME_WATCHING"
return getKeys(folder).mapNotNull {
return getKeys(folder)?.mapNotNull {
it.removePrefix("$folder/").toIntOrNull()
}
}
fun Context.setLastWatched(
fun setLastWatched(
parentId: Int?,
episodeId: Int?,
episode: Int?,
@ -93,12 +94,12 @@ object DataStoreHelper {
)
}
fun Context.removeLastWatched(parentId: Int?) {
fun removeLastWatched(parentId: Int?) {
if (parentId == null) return
removeKey("$currentAccount/$RESULT_RESUME_WATCHING", parentId.toString())
}
fun Context.getLastWatched(id: Int?): VideoDownloadHelper.ResumeWatching? {
fun getLastWatched(id: Int?): VideoDownloadHelper.ResumeWatching? {
if (id == null) return null
return getKey(
"$currentAccount/$RESULT_RESUME_WATCHING",
@ -106,27 +107,35 @@ object DataStoreHelper {
)
}
fun Context.setBookmarkedData(id: Int?, data: BookmarkedData) {
fun setBookmarkedData(id: Int?, data: BookmarkedData) {
if (id == null) return
setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data)
}
fun Context.getBookmarkedData(id: Int?): BookmarkedData? {
fun getBookmarkedData(id: Int?): BookmarkedData? {
if (id == null) return null
return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
}
fun Context.setViewPos(id: Int?, pos: Long, dur: Long) {
fun setViewPos(id: Int?, pos: Long, dur: Long) {
if (id == null) return
if (dur < 10_000) return // too short
setKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), PosDur(pos, dur))
}
fun Context.getViewPos(id: Int): PosDur? {
fun getViewPos(id: Int): PosDur? {
return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null)
}
fun Context.setResultWatchState(id: Int?, status: Int) {
fun getDub(id: Int): DubStatus {
return DubStatus.values()[getKey("$currentAccount/$RESULT_DUB", id.toString()) ?: 0]
}
fun setDub(id: Int, status: DubStatus) {
setKey("$currentAccount/$RESULT_DUB", id.toString(), status.ordinal)
}
fun setResultWatchState(id: Int?, status: Int) {
if (id == null) return
val folder = "$currentAccount/$RESULT_WATCH_STATE"
if (status == WatchType.NONE.internalId) {
@ -137,23 +146,23 @@ object DataStoreHelper {
}
}
fun Context.getResultWatchState(id: Int): WatchType {
fun getResultWatchState(id: Int): WatchType {
return WatchType.fromInternalId(getKey<Int>("$currentAccount/$RESULT_WATCH_STATE", id.toString(), null))
}
fun Context.getResultSeason(id: Int): Int {
return getKey("$currentAccount/$RESULT_SEASON", id.toString(), -1)!!
fun getResultSeason(id: Int): Int {
return getKey("$currentAccount/$RESULT_SEASON", id.toString()) ?: -1
}
fun Context.setResultSeason(id: Int, value: Int?) {
fun setResultSeason(id: Int, value: Int?) {
setKey("$currentAccount/$RESULT_SEASON", id.toString(), value)
}
fun Context.addSync(id: Int, idPrefix: String, url: String) {
fun addSync(id: Int, idPrefix: String, url: String) {
setKey("${idPrefix}_sync", id.toString(), url)
}
fun Context.getSync(id : Int, idPrefixes : List<String>) : List<String?> {
fun getSync(id: Int, idPrefixes: List<String>): List<String?> {
return idPrefixes.map { idPrefix ->
getKey("${idPrefix}_sync", id.toString())
}

View File

@ -5,10 +5,10 @@ import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO
import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_PACKAGE
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadCheck
@ -62,8 +62,8 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo
}
private fun removeKeys(key: String) {
applicationContext.removeKey(WORK_KEY_INFO, key)
applicationContext.removeKey(WORK_KEY_PACKAGE, key)
removeKey(WORK_KEY_INFO, key)
removeKey(WORK_KEY_PACKAGE, key)
}
private suspend fun awaitDownload(id: Int) {

View File

@ -22,6 +22,8 @@ import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.fasterxml.jackson.annotation.JsonProperty
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
@ -30,7 +32,6 @@ import com.lagradost.cloudstream3.services.VideoDownloadService
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -150,16 +151,16 @@ object VideoDownloadManager {
private const val SUCCESS_DOWNLOAD_DONE = 1
private const val SUCCESS_STREAM = 3
private const val SUCCESS_STOPPED = 2
private const val ERROR_DELETING_FILE =
3 // will not download the next one, but is still classified as an error
// will not download the next one, but is still classified as an error
private const val ERROR_DELETING_FILE = 3
private const val ERROR_CREATE_FILE = -2
private const val ERROR_UNKNOWN = -10
private const val ERROR_OPEN_FILE = -3
//private const val ERROR_OPEN_FILE = -3
private const val ERROR_TOO_SMALL_CONNECTION = -4
private const val ERROR_WRONG_CONTENT = -5
//private const val ERROR_WRONG_CONTENT = -5
private const val ERROR_CONNECTION_ERROR = -6
private const val ERROR_MEDIA_STORE_URI_CANT_BE_CREATED = -7
private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8
//private const val ERROR_MEDIA_STORE_URI_CANT_BE_CREATED = -7
//private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8
private const val ERROR_CONTENT_RESOLVER_NOT_FOUND = -9
private const val KEY_RESUME_PACKAGES = "download_resume"
@ -460,7 +461,7 @@ object VideoDownloadManager {
val base = basePathToFile(context, basePath)
val folder = base?.gotoDir(relativePath, false)
if (isScopedStorage && base.isDownloadDir()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) {
return context.contentResolver?.getExistingFolderStartName(relativePath)
} else {
// val normalPath =
@ -529,8 +530,6 @@ object VideoDownloadManager {
}
}
val isScopedStorage = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
data class CreateNotificationMetadata(
val type: DownloadType,
val bytesDownloaded: Long,
@ -561,7 +560,7 @@ object VideoDownloadManager {
var resume = tryResume
val baseFile = context.getBasePath()
if (isScopedStorage && baseFile.first?.isDownloadDir() == true) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.first?.isDownloadDir() == true) {
val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
val currentExistingFile =
@ -658,7 +657,7 @@ object VideoDownloadManager {
val displayName = getDisplayName(name, extension)
val relativePath =
if (isScopedStorage && basePath.first.isDownloadDir()) getRelativePath(folder) else folder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath(folder) else folder
fun deleteFile(): Int {
return delete(context, name, relativePath, extension, parentId, basePath.first)
@ -720,7 +719,7 @@ object VideoDownloadManager {
if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
parentId?.let {
context.setKey(
setKey(
KEY_DOWNLOAD_INFO,
it.toString(),
DownloadedFileInfo(
@ -798,8 +797,8 @@ object VideoDownloadManager {
}
DownloadActionType.Stop -> {
isStopped = true; updateNotification()
context.removeKey(KEY_RESUME_PACKAGES, event.first.toString())
saveQueue(context)
removeKey(KEY_RESUME_PACKAGES, event.first.toString())
saveQueue()
}
DownloadActionType.Resume -> {
isPaused = false; updateNotification()
@ -1025,7 +1024,7 @@ object VideoDownloadManager {
val displayName = getDisplayName(name, extension)
// If scoped storage and using download dir (not accessible with UniFile)
if (isScopedStorage && basePath.isDownloadDir()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) {
val relativePath = getRelativePath(folder)
val lastContent =
context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
@ -1081,7 +1080,7 @@ object VideoDownloadManager {
val basePath = context.getBasePath()
val relativePath =
if (isScopedStorage && basePath.first.isDownloadDir()) getRelativePath(folder) else folder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath(folder) else folder
val stream = setupStream(context, name, relativePath, extension, realIndex > 0)
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
@ -1119,7 +1118,7 @@ object VideoDownloadManager {
fun updateInfo() {
parentId?.let {
context.setKey(
setKey(
KEY_DOWNLOAD_INFO,
it.toString(),
DownloadedFileInfo(
@ -1364,7 +1363,7 @@ object VideoDownloadManager {
val link = item.links[index]
val resume = pkg.linkIndex == index
context.setKey(
setKey(
KEY_RESUME_PACKAGES,
id.toString(),
DownloadResumePackage(item, index)
@ -1383,7 +1382,7 @@ object VideoDownloadManager {
}
}
if (connectionResult != null && connectionResult > 0) { // SUCCESS
context.removeKey(KEY_RESUME_PACKAGES, id.toString())
removeKey(KEY_RESUME_PACKAGES, id.toString())
break
}
}
@ -1410,7 +1409,7 @@ object VideoDownloadManager {
context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null
val base = basePathToFile(context, info.basePath)
if (isScopedStorage && base.isDownloadDir()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) {
val cr = context.contentResolver ?: return null
val fileUri =
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) ?: return null
@ -1457,7 +1456,7 @@ object VideoDownloadManager {
downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped))
downloadDeleteEvent.invoke(id)
val base = basePathToFile(context, info.basePath)
if (isScopedStorage && base.isDownloadDir()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) {
val cr = context.contentResolver ?: return false
val fileUri =
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName)
@ -1505,7 +1504,7 @@ object VideoDownloadManager {
// }
downloadQueue.addLast(pkg)
downloadCheck(context, notificationCallback)
if (setKey) saveQueue(context)
if (setKey) saveQueue()
} else {
downloadEvent.invoke(
Pair(pkg.item.ep.id, DownloadActionType.Resume)
@ -1513,12 +1512,12 @@ object VideoDownloadManager {
}
}
private fun saveQueue(context: Context) {
private fun saveQueue() {
val dQueue =
downloadQueue.toList()
.mapIndexed { index, any -> DownloadQueueResumePackage(index, any) }
.toTypedArray()
context.setKey(KEY_RESUME_QUEUE_PACKAGES, dQueue)
setKey(KEY_RESUME_QUEUE_PACKAGES, dQueue)
}
/*fun isMyServiceRunning(context: Context, serviceClass: Class<*>): Boolean {
@ -1576,7 +1575,7 @@ object VideoDownloadManager {
pkg: DownloadResumePackage,
) {
val key = pkg.item.ep.id.toString()
context.setKey(WORK_KEY_PACKAGE, key, pkg)
setKey(WORK_KEY_PACKAGE, key, pkg)
startWork(context, key)
}
@ -1596,7 +1595,7 @@ object VideoDownloadManager {
)
val key = info.ep.id.toString()
context.setKey(WORK_KEY_INFO, key, info)
setKey(WORK_KEY_INFO, key, info)
startWork(context, key)
}

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.movieproviders.ThenosProvider
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper