mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Clean up and mark questionable code issues (#1209)
This commit is contained in:
parent
0aa48f335a
commit
04dda008c4
97 changed files with 563 additions and 721 deletions
|
@ -35,6 +35,7 @@ import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
@ -81,14 +82,8 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||||
ACRA.errorReporter.handleException(error)
|
ACRA.errorReporter.handleException(error)
|
||||||
try {
|
try {
|
||||||
PrintStream(errorFile).use { ps ->
|
PrintStream(errorFile).use { ps ->
|
||||||
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
|
ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
|
||||||
ps.println(
|
ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
|
||||||
String.format(
|
|
||||||
"Fatal exception on thread %s (%d)",
|
|
||||||
thread.name,
|
|
||||||
thread.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
error.printStackTrace(ps)
|
error.printStackTrace(ps)
|
||||||
}
|
}
|
||||||
} catch (ignored: FileNotFoundException) {
|
} catch (ignored: FileNotFoundException) {
|
||||||
|
@ -106,7 +101,6 @@ class AcraApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
//NativeCrashHandler.initCrashHandler()
|
|
||||||
ExceptionHandler(filesDir.resolve("last_error")) {
|
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||||
|
|
|
@ -164,7 +164,7 @@ object CommonActivity {
|
||||||
val toast = Toast(act)
|
val toast = Toast(act)
|
||||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||||
toast.view = binding.root
|
toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
|
||||||
currentToast = toast
|
currentToast = toast
|
||||||
toast.show()
|
toast.show()
|
||||||
|
|
||||||
|
@ -464,20 +464,6 @@ object CommonActivity {
|
||||||
|
|
||||||
|
|
||||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
||||||
//println("Keycode: $keyCode")
|
|
||||||
//showToast(
|
|
||||||
// this,
|
|
||||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
|
||||||
// Toast.LENGTH_LONG
|
|
||||||
//)
|
|
||||||
|
|
||||||
// Tested keycodes on remote:
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_REWIND
|
|
||||||
// KeyEvent.KEYCODE_MENU
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_NEXT
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
|
||||||
|
|
||||||
// 149 keycode_numpad 5
|
// 149 keycode_numpad 5
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
|
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
|
||||||
private val client: OkHttpClient
|
private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
||||||
override fun execute(request: Request): Response {
|
override fun execute(request: Request): Response {
|
||||||
val httpMethod: String = request.httpMethod()
|
val httpMethod: String = request.httpMethod()
|
||||||
val url: String = request.url()
|
val url: String = request.url()
|
||||||
|
@ -74,8 +74,4 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -82,13 +82,13 @@ import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
|
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
|
||||||
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
||||||
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
|
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
|
@ -347,7 +347,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
println("Repository url: $realUrl")
|
println("Repository url: $realUrl")
|
||||||
loadRepository(realUrl)
|
loadRepository(realUrl)
|
||||||
return true
|
return true
|
||||||
} else if (str.contains(appString)) {
|
} else if (str.contains(APP_STRING)) {
|
||||||
for (api in OAuth2Apis) {
|
for (api in OAuth2Apis) {
|
||||||
if (str.contains("/${api.redirectUrl}")) {
|
if (str.contains("/${api.redirectUrl}")) {
|
||||||
ioSafe {
|
ioSafe {
|
||||||
|
@ -377,15 +377,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
}
|
}
|
||||||
// This specific intent is used for the gradle deployWithAdb
|
// This specific intent is used for the gradle deployWithAdb
|
||||||
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
|
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
|
||||||
if (str == "$appString:") {
|
if (str == "$APP_STRING:") {
|
||||||
PluginManager.hotReloadAllLocalPlugins(activity)
|
PluginManager.hotReloadAllLocalPlugins(activity)
|
||||||
}
|
}
|
||||||
} else if (safeURI(str)?.scheme == appStringRepo) {
|
} else if (safeURI(str)?.scheme == APP_STRING_REPO) {
|
||||||
val url = str.replaceFirst(appStringRepo, "https")
|
val url = str.replaceFirst(APP_STRING_REPO, "https")
|
||||||
loadRepository(url)
|
loadRepository(url)
|
||||||
return true
|
return true
|
||||||
} else if (safeURI(str)?.scheme == appStringSearch) {
|
} else if (safeURI(str)?.scheme == APP_STRING_SEARCH) {
|
||||||
val query = str.substringAfter("$appStringSearch://")
|
val query = str.substringAfter("$APP_STRING_SEARCH://")
|
||||||
nextSearchQuery =
|
nextSearchQuery =
|
||||||
try {
|
try {
|
||||||
URLDecoder.decode(query, "UTF-8")
|
URLDecoder.decode(query, "UTF-8")
|
||||||
|
@ -399,7 +399,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
R.id.navigation_search
|
R.id.navigation_search
|
||||||
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
|
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
|
||||||
R.id.navigation_search
|
R.id.navigation_search
|
||||||
} else if (safeURI(str)?.scheme == appStringPlayer) {
|
} else if (safeURI(str)?.scheme == APP_STRING_PLAYER) {
|
||||||
val uri = Uri.parse(str)
|
val uri = Uri.parse(str)
|
||||||
val name = uri.getQueryParameter("name")
|
val name = uri.getQueryParameter("name")
|
||||||
val url = URLDecoder.decode(uri.authority, "UTF-8")
|
val url = URLDecoder.decode(uri.authority, "UTF-8")
|
||||||
|
@ -413,9 +413,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else if (safeURI(str)?.scheme == appStringResumeWatching) {
|
} else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) {
|
||||||
val id =
|
val id =
|
||||||
str.substringAfter("$appStringResumeWatching://").toIntOrNull()
|
str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull()
|
||||||
?: return false
|
?: return false
|
||||||
ioSafe {
|
ioSafe {
|
||||||
val resumeWatchingCard =
|
val resumeWatchingCard =
|
||||||
|
@ -469,7 +469,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
) DubStatus.Dubbed else DubStatus.Subbed, null
|
) DubStatus.Dubbed else DubStatus.Subbed, null
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
viewModel.loadSmall(this, result)
|
viewModel.loadSmall(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,7 +605,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
}
|
}
|
||||||
|
|
||||||
//private var mCastSession: CastSession? = null
|
//private var mCastSession: CastSession? = null
|
||||||
lateinit var mSessionManager: SessionManager
|
var mSessionManager: SessionManager? = null
|
||||||
private val mSessionManagerListener: SessionManagerListener<Session> by lazy { SessionManagerListenerImpl() }
|
private val mSessionManagerListener: SessionManagerListener<Session> by lazy { SessionManagerListenerImpl() }
|
||||||
|
|
||||||
private inner class SessionManagerListenerImpl : SessionManagerListener<Session> {
|
private inner class SessionManagerListenerImpl : SessionManagerListener<Session> {
|
||||||
|
@ -645,8 +645,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
setActivityInstance(this)
|
setActivityInstance(this)
|
||||||
try {
|
try {
|
||||||
if (isCastApiAvailable()) {
|
if (isCastApiAvailable()) {
|
||||||
//mCastSession = mSessionManager.currentCastSession
|
mSessionManager?.addSessionManagerListener(mSessionManagerListener)
|
||||||
mSessionManager.addSessionManagerListener(mSessionManagerListener)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -662,7 +661,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (isCastApiAvailable()) {
|
if (isCastApiAvailable()) {
|
||||||
mSessionManager.removeSessionManagerListener(mSessionManagerListener)
|
mSessionManager?.removeSessionManagerListener(mSessionManagerListener)
|
||||||
//mCastSession = null
|
//mCastSession = null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -766,7 +765,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
list.forEach { custom ->
|
list.forEach { custom ->
|
||||||
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
|
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
|
||||||
?.let {
|
?.let {
|
||||||
allProviders.add(it.javaClass.newInstance().apply {
|
allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply {
|
||||||
name = custom.name
|
name = custom.name
|
||||||
lang = custom.lang
|
lang = custom.lang
|
||||||
mainUrl = custom.url.trimEnd('/')
|
mainUrl = custom.url.trimEnd('/')
|
||||||
|
@ -1147,7 +1146,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
try {
|
try {
|
||||||
if (isCastApiAvailable()) {
|
if (isCastApiAvailable()) {
|
||||||
mSessionManager = CastContext.getSharedInstance(this).sessionManager
|
CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager }
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logError(t)
|
logError(t)
|
||||||
|
@ -1449,13 +1448,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
val value = viewModel.watchStatus.value ?: WatchType.NONE
|
val value = viewModel.watchStatus.value ?: WatchType.NONE
|
||||||
|
|
||||||
this@MainActivity.showBottomDialog(
|
this@MainActivity.showBottomDialog(
|
||||||
WatchType.values().map { getString(it.stringRes) }.toList(),
|
WatchType.entries.map { getString(it.stringRes) }.toList(),
|
||||||
value.ordinal,
|
value.ordinal,
|
||||||
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
||||||
showApply = false,
|
showApply = false,
|
||||||
{}) {
|
{}) {
|
||||||
viewModel.updateWatchStatus(
|
viewModel.updateWatchStatus(
|
||||||
WatchType.values()[it],
|
WatchType.entries[it],
|
||||||
this@MainActivity
|
this@MainActivity
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1465,12 +1464,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
||||||
?: SyncWatchType.NONE
|
?: SyncWatchType.NONE
|
||||||
|
|
||||||
this@MainActivity.showBottomDialog(
|
this@MainActivity.showBottomDialog(
|
||||||
SyncWatchType.values().map { getString(it.stringRes) }.toList(),
|
SyncWatchType.entries.map { getString(it.stringRes) }.toList(),
|
||||||
value.ordinal,
|
value.ordinal,
|
||||||
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
||||||
showApply = false,
|
showApply = false,
|
||||||
{}) {
|
{}) {
|
||||||
syncViewModel.setStatus(SyncWatchType.values()[it].internalId)
|
syncViewModel.setStatus(SyncWatchType.entries[it].internalId)
|
||||||
syncViewModel.publishUserData()
|
syncViewModel.publishUserData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
package com.lagradost.cloudstream3
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
object NativeCrashHandler {
|
|
||||||
// external fun triggerNativeCrash()
|
|
||||||
/*private external fun initNativeCrashHandler()
|
|
||||||
private external fun getSignalStatus(): Int
|
|
||||||
|
|
||||||
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
|
|
||||||
//launch {
|
|
||||||
// delay(10000)
|
|
||||||
// triggerNativeCrash()
|
|
||||||
//}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
delay(10_000)
|
|
||||||
val signal = getSignalStatus()
|
|
||||||
// Signal is initialized to zero
|
|
||||||
if (signal == 0) continue
|
|
||||||
|
|
||||||
// Do not crash in safe mode!
|
|
||||||
if (lastError != null) continue
|
|
||||||
if (checkSafeModeFile()) continue
|
|
||||||
|
|
||||||
AcraApplication.exceptionHandler?.uncaughtException(
|
|
||||||
Thread.currentThread(),
|
|
||||||
RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initCrashHandler() {
|
|
||||||
try {
|
|
||||||
System.loadLibrary("native-lib")
|
|
||||||
initNativeCrashHandler()
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
// Make debug crash.
|
|
||||||
if (BuildConfig.DEBUG) throw t
|
|
||||||
logError(t)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initSignalPolling()
|
|
||||||
}*/
|
|
||||||
}
|
|
|
@ -2,15 +2,13 @@ package com.lagradost.cloudstream3.metaproviders
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.MainAPI
|
import com.lagradost.cloudstream3.MainAPI
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
|
||||||
object SyncRedirector {
|
object SyncRedirector {
|
||||||
val syncApis = SyncApis
|
|
||||||
private val syncIds =
|
private val syncIds =
|
||||||
listOf(
|
listOf(
|
||||||
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
|
SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""),
|
||||||
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
|
SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""")
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun redirect(
|
suspend fun redirect(
|
||||||
|
|
|
@ -296,7 +296,7 @@ open class TraktProvider : MainAPI() {
|
||||||
return try {
|
return try {
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
||||||
APIHolder.unixTimeMS < dateTime
|
unixTimeMS < dateTime
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logError(t)
|
logError(t)
|
||||||
false
|
false
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.network
|
package com.lagradost.cloudstream3.network
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
|
@ -10,7 +9,10 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.nicehttp.Requests.Companion.await
|
import com.lagradost.nicehttp.Requests.Companion.await
|
||||||
import com.lagradost.nicehttp.cookies
|
import com.lagradost.nicehttp.cookies
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.*
|
import okhttp3.Headers
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,5 +2,4 @@ package com.lagradost.cloudstream3.plugins
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class CloudstreamPlugin(
|
annotation class CloudstreamPlugin
|
||||||
)
|
|
|
@ -34,7 +34,7 @@ abstract class Plugin {
|
||||||
*/
|
*/
|
||||||
fun registerMainAPI(element: MainAPI) {
|
fun registerMainAPI(element: MainAPI) {
|
||||||
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
||||||
element.sourcePlugin = this.__filename
|
element.sourcePlugin = this.filename
|
||||||
// Race condition causing which would case duplicates if not for distinctBy
|
// Race condition causing which would case duplicates if not for distinctBy
|
||||||
synchronized(APIHolder.allProviders) {
|
synchronized(APIHolder.allProviders) {
|
||||||
APIHolder.allProviders.add(element)
|
APIHolder.allProviders.add(element)
|
||||||
|
@ -48,7 +48,7 @@ abstract class Plugin {
|
||||||
*/
|
*/
|
||||||
fun registerExtractorAPI(element: ExtractorApi) {
|
fun registerExtractorAPI(element: ExtractorApi) {
|
||||||
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi")
|
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi")
|
||||||
element.sourcePlugin = this.__filename
|
element.sourcePlugin = this.filename
|
||||||
extractorApis.add(element)
|
extractorApis.add(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,11 @@ abstract class Plugin {
|
||||||
*/
|
*/
|
||||||
var resources: Resources? = null
|
var resources: Resources? = null
|
||||||
/** Full file path to the plugin. */
|
/** Full file path to the plugin. */
|
||||||
var __filename: String? = null
|
@Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename"))
|
||||||
|
var __filename: String?
|
||||||
|
get() = filename
|
||||||
|
set(value) {filename = value}
|
||||||
|
var filename: String? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will add a button in the settings allowing you to add custom settings
|
* This will add a button in the settings allowing you to add custom settings
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package com.lagradost.cloudstream3.plugins
|
package com.lagradost.cloudstream3.plugins
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
@ -163,7 +166,7 @@ object PluginManager {
|
||||||
|
|
||||||
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
|
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
|
||||||
|
|
||||||
public var currentlyLoading: String? = null
|
var currentlyLoading: String? = null
|
||||||
|
|
||||||
// Maps filepath to plugin
|
// Maps filepath to plugin
|
||||||
val plugins: MutableMap<String, Plugin> =
|
val plugins: MutableMap<String, Plugin> =
|
||||||
|
@ -339,7 +342,7 @@ object PluginManager {
|
||||||
|
|
||||||
//Omit non-NSFW if mode is set to NSFW only
|
//Omit non-NSFW if mode is set to NSFW only
|
||||||
if (mode == AutoDownloadMode.NsfwOnly) {
|
if (mode == AutoDownloadMode.NsfwOnly) {
|
||||||
if (tvtypes.contains(TvType.NSFW.name) == false) {
|
if (!tvtypes.contains(TvType.NSFW.name)) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -504,10 +507,12 @@ object PluginManager {
|
||||||
val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also {
|
val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also {
|
||||||
Log.d(TAG, "No manifest version for ${data.internalName}")
|
Log.d(TAG, "No manifest version for ${data.internalName}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val pluginClass: Class<*> =
|
val pluginClass: Class<*> =
|
||||||
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
|
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
|
||||||
val pluginInstance: Plugin =
|
val pluginInstance: Plugin =
|
||||||
pluginClass.newInstance() as Plugin
|
pluginClass.getDeclaredConstructor().newInstance() as Plugin
|
||||||
|
|
||||||
// Sets with the proper version
|
// Sets with the proper version
|
||||||
setPluginData(data.copy(version = version))
|
setPluginData(data.copy(version = version))
|
||||||
|
@ -517,14 +522,16 @@ object PluginManager {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginInstance.__filename = file.absolutePath
|
pluginInstance.filename = file.absolutePath
|
||||||
if (manifest.requiresResources) {
|
if (manifest.requiresResources) {
|
||||||
Log.d(TAG, "Loading resources for ${data.internalName}")
|
Log.d(TAG, "Loading resources for ${data.internalName}")
|
||||||
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
|
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
|
||||||
val assets = AssetManager::class.java.newInstance()
|
val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()
|
||||||
val addAssetPath =
|
val addAssetPath =
|
||||||
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
|
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
|
||||||
addAssetPath.invoke(assets, file.absolutePath)
|
addAssetPath.invoke(assets, file.absolutePath)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
pluginInstance.resources = Resources(
|
pluginInstance.resources = Resources(
|
||||||
assets,
|
assets,
|
||||||
context.resources.displayMetrics,
|
context.resources.displayMetrics,
|
||||||
|
@ -566,14 +573,14 @@ object PluginManager {
|
||||||
|
|
||||||
// remove all registered apis
|
// remove all registered apis
|
||||||
synchronized(APIHolder.apis) {
|
synchronized(APIHolder.apis) {
|
||||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
|
||||||
removePluginMapping(it)
|
removePluginMapping(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
synchronized(APIHolder.allProviders) {
|
synchronized(APIHolder.allProviders) {
|
||||||
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
|
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
|
||||||
}
|
}
|
||||||
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
|
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
|
||||||
|
|
||||||
classLoaders.values.removeIf { v -> v == plugin }
|
classLoaders.values.removeIf { v -> v == plugin }
|
||||||
|
|
||||||
|
@ -720,9 +727,14 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
val notification = builder.build()
|
val notification = builder.build()
|
||||||
with(NotificationManagerCompat.from(context)) {
|
// notificationId is a unique int for each notification that you must define
|
||||||
// notificationId is a unique int for each notification that you must define
|
if (ActivityCompat.checkSelfPermission(
|
||||||
notify((System.currentTimeMillis() / 1000).toInt(), notification)
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
NotificationManagerCompat.from(context)
|
||||||
|
.notify((System.currentTimeMillis() / 1000).toInt(), notification)
|
||||||
}
|
}
|
||||||
return notification
|
return notification
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -73,7 +73,7 @@ object RepositoryManager {
|
||||||
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
|
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
|
||||||
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
|
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
|
||||||
}
|
}
|
||||||
val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
|
private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
|
||||||
|
|
||||||
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
|
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
|
||||||
fun convertRawGitUrl(url: String): String {
|
fun convertRawGitUrl(url: String): String {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock
|
||||||
object VotingApi { // please do not cheat the votes lol
|
object VotingApi { // please do not cheat the votes lol
|
||||||
private const val LOGKEY = "VotingApi"
|
private const val LOGKEY = "VotingApi"
|
||||||
|
|
||||||
private const val apiDomain = "https://counterapi.com/api"
|
private const val API_DOMAIN = "https://counterapi.com/api"
|
||||||
|
|
||||||
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
||||||
MessageDigest
|
MessageDigest
|
||||||
|
@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol
|
||||||
.joinToString("-")
|
.joinToString("-")
|
||||||
|
|
||||||
private suspend fun readVote(pluginUrl: String): Int {
|
private suspend fun readVote(pluginUrl: String): Int {
|
||||||
var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
|
val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
|
||||||
Log.d(LOGKEY, "Requesting: $url")
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
return app.get(url).parsedSafe<Result>()?.value ?: 0
|
return app.get(url).parsedSafe<Result>()?.value ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun writeVote(pluginUrl: String): Boolean {
|
private suspend fun writeVote(pluginUrl: String): Boolean {
|
||||||
var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
|
val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
|
||||||
Log.d(LOGKEY, "Requesting: $url")
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
return app.get(url).parsedSafe<Result>()?.value != null
|
return app.get(url).parsedSafe<Result>()?.value != null
|
||||||
}
|
}
|
||||||
|
@ -69,8 +69,7 @@ object VotingApi { // please do not cheat the votes lol
|
||||||
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
|
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
|
||||||
|
|
||||||
fun canVote(pluginUrl: String): Boolean {
|
fun canVote(pluginUrl: String): Boolean {
|
||||||
if (!PluginManager.urlPlugins.contains(pluginUrl)) return false
|
return PluginManager.urlPlugins.contains(pluginUrl)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val voteLock = Mutex()
|
private val voteLock = Mutex()
|
||||||
|
|
|
@ -59,7 +59,7 @@ class SubtitleResource {
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unzip(file: File): List<Pair<String, File>> {
|
private fun unzip(file: File): List<Pair<String, File>> {
|
||||||
val entries = mutableListOf<Pair<String, File>>()
|
val entries = mutableListOf<Pair<String, File>>()
|
||||||
|
|
||||||
ZipInputStream(file.inputStream()).use { zipInputStream ->
|
ZipInputStream(file.inputStream()).use { zipInputStream ->
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.subtitles
|
package com.lagradost.cloudstream3.subtitles
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
|
||||||
class AbstractSubtitleEntities {
|
class AbstractSubtitleEntities {
|
||||||
|
|
|
@ -56,22 +56,22 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
subSourceApi
|
subSourceApi
|
||||||
)
|
)
|
||||||
|
|
||||||
const val appString = "cloudstreamapp"
|
const val APP_STRING = "cloudstreamapp"
|
||||||
const val appStringRepo = "cloudstreamrepo"
|
const val APP_STRING_REPO = "cloudstreamrepo"
|
||||||
const val appStringPlayer = "cloudstreamplayer"
|
const val APP_STRING_PLAYER = "cloudstreamplayer"
|
||||||
|
|
||||||
// Instantly start the search given a query
|
// Instantly start the search given a query
|
||||||
const val appStringSearch = "cloudstreamsearch"
|
const val APP_STRING_SEARCH = "cloudstreamsearch"
|
||||||
|
|
||||||
// Instantly resume watching a show
|
// Instantly resume watching a show
|
||||||
const val appStringResumeWatching = "cloudstreamcontinuewatching"
|
const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching"
|
||||||
|
|
||||||
val unixTime: Long
|
val unixTime: Long
|
||||||
get() = System.currentTimeMillis() / 1000L
|
get() = System.currentTimeMillis() / 1000L
|
||||||
val unixTimeMs: Long
|
val unixTimeMs: Long
|
||||||
get() = System.currentTimeMillis()
|
get() = System.currentTimeMillis()
|
||||||
|
|
||||||
const val maxStale = 60 * 10
|
const val MAX_STALE = 60 * 10
|
||||||
|
|
||||||
fun secondsToReadable(seconds: Int, completedValue: String): String {
|
fun secondsToReadable(seconds: Int, completedValue: String): String {
|
||||||
var secondsLong = seconds.toLong()
|
var secondsLong = seconds.toLong()
|
||||||
|
|
|
@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi {
|
||||||
override fun logOut() {}
|
override fun logOut() {}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val host = "https://www.addic7ed.com"
|
const val HOST = "https://www.addic7ed.com"
|
||||||
const val TAG = "ADDIC7ED"
|
const val TAG = "ADDIC7ED"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fixUrl(url: String): String {
|
private fun fixUrl(url: String): String {
|
||||||
return if (url.startsWith("/")) host + url
|
return if (url.startsWith("/")) HOST + url
|
||||||
else if (!url.startsWith("http")) "$host/$url"
|
else if (!url.startsWith("http")) "$HOST/$url"
|
||||||
else url
|
else url
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = queryText.substringBefore("(").trim()
|
val title = queryText.substringBefore("(").trim()
|
||||||
val url = "$host/search.php?search=${title}&Submit=Search"
|
val url = "$HOST/search.php?search=${title}&Submit=Search"
|
||||||
val hostDocument = app.get(url).document
|
val hostDocument = app.get(url).document
|
||||||
var searchResult = ""
|
var searchResult = ""
|
||||||
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
|
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
|
||||||
|
@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi {
|
||||||
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
|
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
|
||||||
?.substringBefore(",")
|
?.substringBefore(",")
|
||||||
val doc = app.get(
|
val doc = app.get(
|
||||||
"$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
|
"$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
|
||||||
referer = "$host/"
|
referer = "$HOST/"
|
||||||
).document
|
).document
|
||||||
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
|
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
|
||||||
if (node.selectFirst("td")?.text()
|
if (node.selectFirst("td")?.text()
|
||||||
|
@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi {
|
||||||
val link = fixUrl(node.select("a.buttonDownload").attr("href"))
|
val link = fixUrl(node.select("a.buttonDownload").attr("href"))
|
||||||
val isHearingImpaired =
|
val isHearingImpaired =
|
||||||
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
|
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
|
||||||
cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired)
|
cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired)
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun handleRedirect(url: String): Boolean {
|
override suspend fun handleRedirect(url: String): Boolean {
|
||||||
val sanitizer =
|
val sanitizer =
|
||||||
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
|
||||||
val token = sanitizer["access_token"]!!
|
val token = sanitizer["access_token"]!!
|
||||||
val expiresIn = sanitizer["expires_in"]!!
|
val expiresIn = sanitizer["expires_in"]!!
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
||||||
val data = searchShows(name) ?: return null
|
val data = searchShows(name) ?: return null
|
||||||
return data.data?.Page?.media?.map {
|
return data.data?.page?.media?.map {
|
||||||
SyncAPI.SyncSearchResult(
|
SyncAPI.SyncSearchResult(
|
||||||
it.title.romaji ?: return null,
|
it.title.romaji ?: return null,
|
||||||
this.name,
|
this.name,
|
||||||
|
@ -101,7 +101,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override suspend fun getResult(id: String): SyncAPI.SyncResult {
|
override suspend fun getResult(id: String): SyncAPI.SyncResult {
|
||||||
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
|
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
|
||||||
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
|
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
|
||||||
val season = getSeason(internalId).data.Media
|
val season = getSeason(internalId).data.media
|
||||||
|
|
||||||
return SyncAPI.SyncResult(
|
return SyncAPI.SyncResult(
|
||||||
season.id.toString(),
|
season.id.toString(),
|
||||||
|
@ -301,12 +301,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
|
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
|
||||||
val shows = searchShows(name.replace(blackListRegex, ""))
|
val shows = searchShows(name.replace(blackListRegex, ""))
|
||||||
|
|
||||||
shows?.data?.Page?.media?.find {
|
shows?.data?.page?.media?.find {
|
||||||
(malId ?: "NONE") == it.idMal.toString()
|
(malId ?: "NONE") == it.idMal.toString()
|
||||||
}?.let { return it }
|
}?.let { return it }
|
||||||
|
|
||||||
val filtered =
|
val filtered =
|
||||||
shows?.data?.Page?.media?.filter {
|
shows?.data?.page?.media?.filter {
|
||||||
(((it.startDate.year ?: year.toString()) == year.toString()
|
(((it.startDate.year ?: year.toString()) == year.toString()
|
||||||
|| year == null))
|
|| year == null))
|
||||||
}
|
}
|
||||||
|
@ -496,7 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val data = postApi(q, true)
|
val data = postApi(q, true)
|
||||||
val d = parseJson<GetDataRoot>(data ?: return null)
|
val d = parseJson<GetDataRoot>(data ?: return null)
|
||||||
|
|
||||||
val main = d.data?.Media
|
val main = d.data?.media
|
||||||
if (main?.mediaListEntry != null) {
|
if (main?.mediaListEntry != null) {
|
||||||
return AniListTitleHolder(
|
return AniListTitleHolder(
|
||||||
title = main.title,
|
title = main.title,
|
||||||
|
@ -536,7 +536,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Authorization" to "Bearer " + (getAuth()
|
"Authorization" to "Bearer " + (getAuth()
|
||||||
?: return@suspendSafeApiCall null),
|
?: return@suspendSafeApiCall null),
|
||||||
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
|
if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache"
|
||||||
),
|
),
|
||||||
cacheTime = 0,
|
cacheTime = 0,
|
||||||
data = mapOf(
|
data = mapOf(
|
||||||
|
@ -647,7 +647,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
|
@JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getAniListListCached(): Array<Lists>? {
|
private fun getAniListListCached(): Array<Lists>? {
|
||||||
|
@ -659,7 +659,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
if (checkToken()) return null
|
if (checkToken()) return null
|
||||||
return if (requireLibraryRefresh) {
|
return if (requireLibraryRefresh) {
|
||||||
val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray()
|
val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray()
|
||||||
if (list != null) {
|
if (list != null) {
|
||||||
setKey(ANILIST_CACHED_LIST, list)
|
setKey(ANILIST_CACHED_LIST, list)
|
||||||
}
|
}
|
||||||
|
@ -678,7 +678,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
// To fill empty lists when AniList does not return them
|
// To fill empty lists when AniList does not return them
|
||||||
val baseMap =
|
val baseMap =
|
||||||
AniListStatusType.values().filter { it.value >= 0 }.associate {
|
AniListStatusType.entries.filter { it.value >= 0 }.associate {
|
||||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -764,7 +764,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
/** Used to query a saved MediaItem on the list to get the id for removal */
|
/** Used to query a saved MediaItem on the list to get the id for removal */
|
||||||
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
|
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
|
||||||
data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null)
|
data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null)
|
||||||
data class MediaListId(@JsonProperty("id") val id: Long? = null)
|
data class MediaListId(@JsonProperty("id") val id: Long? = null)
|
||||||
|
|
||||||
private suspend fun postDataAboutId(
|
private suspend fun postDataAboutId(
|
||||||
|
@ -787,7 +787,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
"""
|
"""
|
||||||
val response = postApi(idQuery)
|
val response = postApi(idQuery)
|
||||||
val listId =
|
val listId =
|
||||||
tryParseJson<MediaListItemRoot>(response)?.data?.MediaList?.id ?: return false
|
tryParseJson<MediaListItemRoot>(response)?.data?.mediaList?.id ?: return false
|
||||||
"""
|
"""
|
||||||
mutation(${'$'}id: Int = $listId) {
|
mutation(${'$'}id: Int = $listId) {
|
||||||
DeleteMediaListEntry(id: ${'$'}id) {
|
DeleteMediaListEntry(id: ${'$'}id) {
|
||||||
|
@ -836,7 +836,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val data = postApi(q)
|
val data = postApi(q)
|
||||||
if (data.isNullOrBlank()) return null
|
if (data.isNullOrBlank()) return null
|
||||||
val userData = parseJson<AniListRoot>(data)
|
val userData = parseJson<AniListRoot>(data)
|
||||||
val u = userData.data?.Viewer
|
val u = userData.data?.viewer
|
||||||
val user = AniListUser(
|
val user = AniListUser(
|
||||||
u?.id,
|
u?.id,
|
||||||
u?.name,
|
u?.name,
|
||||||
|
@ -858,8 +858,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
suspend fun getSeasonRecursive(id: Int) {
|
suspend fun getSeasonRecursive(id: Int) {
|
||||||
val season = getSeason(id)
|
val season = getSeason(id)
|
||||||
seasons.add(season)
|
seasons.add(season)
|
||||||
if (season.data.Media.format?.startsWith("TV") == true) {
|
if (season.data.media.format?.startsWith("TV") == true) {
|
||||||
season.data.Media.relations?.edges?.forEach {
|
season.data.media.relations?.edges?.forEach {
|
||||||
if (it.node?.format != null) {
|
if (it.node?.format != null) {
|
||||||
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
|
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
|
||||||
getSeasonRecursive(it.node.id)
|
getSeasonRecursive(it.node.id)
|
||||||
|
@ -878,7 +878,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SeasonData(
|
data class SeasonData(
|
||||||
@JsonProperty("Media") val Media: SeasonMedia,
|
@JsonProperty("Media") val media: SeasonMedia,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SeasonMedia(
|
data class SeasonMedia(
|
||||||
|
@ -1050,7 +1050,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class AniListData(
|
data class AniListData(
|
||||||
@JsonProperty("Viewer") val Viewer: AniListViewer?,
|
@JsonProperty("Viewer") val viewer: AniListViewer?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class AniListRoot(
|
data class AniListRoot(
|
||||||
|
@ -1090,7 +1090,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class LikeData(
|
data class LikeData(
|
||||||
@JsonProperty("Viewer") val Viewer: LikeViewer?,
|
@JsonProperty("Viewer") val viewer: LikeViewer?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class LikeRoot(
|
data class LikeRoot(
|
||||||
|
@ -1130,7 +1130,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetDataData(
|
data class GetDataData(
|
||||||
@JsonProperty("Media") val Media: GetDataMedia?,
|
@JsonProperty("Media") val media: GetDataMedia?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetDataRoot(
|
data class GetDataRoot(
|
||||||
|
@ -1163,7 +1163,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetSearchPage(
|
data class GetSearchPage(
|
||||||
@JsonProperty("Page") val Page: GetSearchData?,
|
@JsonProperty("Page") val page: GetSearchData?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetSearchData(
|
data class GetSearchData(
|
||||||
|
|
|
@ -119,8 +119,6 @@ class LocalList : SyncAPI {
|
||||||
ListSorting.AlphabeticalZ,
|
ListSorting.AlphabeticalZ,
|
||||||
ListSorting.UpdatedNew,
|
ListSorting.UpdatedNew,
|
||||||
ListSorting.UpdatedOld,
|
ListSorting.UpdatedOld,
|
||||||
// ListSorting.RatingHigh,
|
|
||||||
// ListSorting.RatingLow,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,14 +19,18 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
import com.lagradost.cloudstream3.ui.SyncWatchType
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
|
||||||
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
|
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.time.Instant
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
/** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */
|
/** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */
|
||||||
const val MAL_MAX_SEARCH_LIMIT = 25
|
const val MAL_MAX_SEARCH_LIMIT = 25
|
||||||
|
@ -51,7 +55,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||||
//getMalUser(true)?
|
|
||||||
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
|
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
|
||||||
return AuthAPI.LoginInfo(
|
return AuthAPI.LoginInfo(
|
||||||
profilePicture = user.picture,
|
profilePicture = user.picture,
|
||||||
|
@ -84,7 +87,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
this.name,
|
this.name,
|
||||||
node.id.toString(),
|
node.id.toString(),
|
||||||
"$mainUrl/anime/${node.id}/",
|
"$mainUrl/anime/${node.id}/",
|
||||||
node.main_picture?.large ?: node.main_picture?.medium
|
node.mainPicture?.large ?: node.mainPicture?.medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,7 +181,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
private fun parseDate(string: String?): Long? {
|
private fun parseDate(string: String?): Long? {
|
||||||
return try {
|
return try {
|
||||||
SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time
|
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -190,7 +193,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
apiName = this.name,
|
apiName = this.name,
|
||||||
syncId = node.id.toString(),
|
syncId = node.id.toString(),
|
||||||
url = "$mainUrl/anime/${node.id}",
|
url = "$mainUrl/anime/${node.id}",
|
||||||
posterUrl = node.main_picture?.large
|
posterUrl = node.mainPicture?.large
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,12 +247,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
|
|
||||||
val data =
|
val data =
|
||||||
getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status")
|
getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status")
|
||||||
return SyncAPI.SyncStatus(
|
return SyncAPI.SyncStatus(
|
||||||
score = data?.score,
|
score = data?.score,
|
||||||
status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) ,
|
status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)),
|
||||||
isFavorite = null,
|
isFavorite = null,
|
||||||
watchedEpisodes = data?.num_episodes_watched,
|
watchedEpisodes = data?.numEpisodesWatched,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
private fun parseDateLong(string: String?): Long? {
|
private fun parseDateLong(string: String?): Long? {
|
||||||
return try {
|
return try {
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse(
|
||||||
string ?: return null
|
string ?: return null
|
||||||
)?.time?.div(1000)
|
)?.time?.div(1000)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -302,7 +305,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun handleRedirect(url: String): Boolean {
|
override suspend fun handleRedirect(url: String): Boolean {
|
||||||
val sanitizer =
|
val sanitizer =
|
||||||
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
|
||||||
val state = sanitizer["state"]!!
|
val state = sanitizer["state"]!!
|
||||||
if (state == "RequestID$requestId") {
|
if (state == "RequestID$requestId") {
|
||||||
val currentCode = sanitizer["code"]!!
|
val currentCode = sanitizer["code"]!!
|
||||||
|
@ -351,9 +354,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
try {
|
try {
|
||||||
if (response != "") {
|
if (response != "") {
|
||||||
val token = parseJson<ResponseToken>(response)
|
val token = parseJson<ResponseToken>(response)
|
||||||
setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
|
setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime))
|
||||||
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
|
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken)
|
||||||
setKey(accountId, MAL_TOKEN_KEY, token.access_token)
|
setKey(accountId, MAL_TOKEN_KEY, token.accessToken)
|
||||||
requireLibraryRefresh = true
|
requireLibraryRefresh = true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -395,53 +398,53 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
data class Node(
|
data class Node(
|
||||||
@JsonProperty("id") val id: Int,
|
@JsonProperty("id") val id: Int,
|
||||||
@JsonProperty("title") val title: String,
|
@JsonProperty("title") val title: String,
|
||||||
@JsonProperty("main_picture") val main_picture: MainPicture?,
|
@JsonProperty("main_picture") val mainPicture: MainPicture?,
|
||||||
@JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?,
|
@JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?,
|
||||||
@JsonProperty("media_type") val media_type: String?,
|
@JsonProperty("media_type") val mediaType: String?,
|
||||||
@JsonProperty("num_episodes") val num_episodes: Int?,
|
@JsonProperty("num_episodes") val numEpisodes: Int?,
|
||||||
@JsonProperty("status") val status: String?,
|
@JsonProperty("status") val status: String?,
|
||||||
@JsonProperty("start_date") val start_date: String?,
|
@JsonProperty("start_date") val startDate: String?,
|
||||||
@JsonProperty("end_date") val end_date: String?,
|
@JsonProperty("end_date") val endDate: String?,
|
||||||
@JsonProperty("average_episode_duration") val average_episode_duration: Int?,
|
@JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?,
|
||||||
@JsonProperty("synopsis") val synopsis: String?,
|
@JsonProperty("synopsis") val synopsis: String?,
|
||||||
@JsonProperty("mean") val mean: Double?,
|
@JsonProperty("mean") val mean: Double?,
|
||||||
@JsonProperty("genres") val genres: List<Genres>?,
|
@JsonProperty("genres") val genres: List<Genres>?,
|
||||||
@JsonProperty("rank") val rank: Int?,
|
@JsonProperty("rank") val rank: Int?,
|
||||||
@JsonProperty("popularity") val popularity: Int?,
|
@JsonProperty("popularity") val popularity: Int?,
|
||||||
@JsonProperty("num_list_users") val num_list_users: Int?,
|
@JsonProperty("num_list_users") val numListUsers: Int?,
|
||||||
@JsonProperty("num_favorites") val num_favorites: Int?,
|
@JsonProperty("num_favorites") val numFavorites: Int?,
|
||||||
@JsonProperty("num_scoring_users") val num_scoring_users: Int?,
|
@JsonProperty("num_scoring_users") val numScoringUsers: Int?,
|
||||||
@JsonProperty("start_season") val start_season: StartSeason?,
|
@JsonProperty("start_season") val startSeason: StartSeason?,
|
||||||
@JsonProperty("broadcast") val broadcast: Broadcast?,
|
@JsonProperty("broadcast") val broadcast: Broadcast?,
|
||||||
@JsonProperty("nsfw") val nsfw: String?,
|
@JsonProperty("nsfw") val nsfw: String?,
|
||||||
@JsonProperty("created_at") val created_at: String?,
|
@JsonProperty("created_at") val createdAt: String?,
|
||||||
@JsonProperty("updated_at") val updated_at: String?
|
@JsonProperty("updated_at") val updatedAt: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ListStatus(
|
data class ListStatus(
|
||||||
@JsonProperty("status") val status: String?,
|
@JsonProperty("status") val status: String?,
|
||||||
@JsonProperty("score") val score: Int,
|
@JsonProperty("score") val score: Int,
|
||||||
@JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
|
@JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
|
||||||
@JsonProperty("is_rewatching") val is_rewatching: Boolean,
|
@JsonProperty("is_rewatching") val isRewatching: Boolean,
|
||||||
@JsonProperty("updated_at") val updated_at: String,
|
@JsonProperty("updated_at") val updatedAt: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("node") val node: Node,
|
@JsonProperty("node") val node: Node,
|
||||||
@JsonProperty("list_status") val list_status: ListStatus?,
|
@JsonProperty("list_status") val listStatus: ListStatus?,
|
||||||
) {
|
) {
|
||||||
fun toLibraryItem(): SyncAPI.LibraryItem {
|
fun toLibraryItem(): SyncAPI.LibraryItem {
|
||||||
return SyncAPI.LibraryItem(
|
return SyncAPI.LibraryItem(
|
||||||
this.node.title,
|
this.node.title,
|
||||||
"https://myanimelist.net/anime/${this.node.id}/",
|
"https://myanimelist.net/anime/${this.node.id}/",
|
||||||
this.node.id.toString(),
|
this.node.id.toString(),
|
||||||
this.list_status?.num_episodes_watched,
|
this.listStatus?.numEpisodesWatched,
|
||||||
this.node.num_episodes,
|
this.node.numEpisodes,
|
||||||
this.list_status?.score?.times(10),
|
this.listStatus?.score?.times(10),
|
||||||
parseDateLong(this.list_status?.updated_at),
|
parseDateLong(this.listStatus?.updatedAt),
|
||||||
"MAL",
|
"MAL",
|
||||||
TvType.Anime,
|
TvType.Anime,
|
||||||
this.node.main_picture?.large ?: this.node.main_picture?.medium,
|
this.node.mainPicture?.large ?: this.node.mainPicture?.medium,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
plot = this.node.synopsis,
|
plot = this.node.synopsis,
|
||||||
|
@ -470,8 +473,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Broadcast(
|
data class Broadcast(
|
||||||
@JsonProperty("day_of_the_week") val day_of_the_week: String?,
|
@JsonProperty("day_of_the_week") val dayOfTheWeek: String?,
|
||||||
@JsonProperty("start_time") val start_time: String?
|
@JsonProperty("start_time") val startTime: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getMalAnimeListCached(): Array<Data>? {
|
private fun getMalAnimeListCached(): Array<Data>? {
|
||||||
|
@ -491,14 +494,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
||||||
val list = getMalAnimeListSmart()?.groupBy {
|
val list = getMalAnimeListSmart()?.groupBy {
|
||||||
convertToStatus(it.list_status?.status ?: "").stringRes
|
convertToStatus(it.listStatus?.status ?: "").stringRes
|
||||||
}?.mapValues { group ->
|
}?.mapValues { group ->
|
||||||
group.value.map { it.toLibraryItem() }
|
group.value.map { it.toLibraryItem() }
|
||||||
} ?: emptyMap()
|
} ?: emptyMap()
|
||||||
|
|
||||||
// To fill empty lists when MAL does not return them
|
// To fill empty lists when MAL does not return them
|
||||||
val baseMap =
|
val baseMap =
|
||||||
MalStatusType.values().filter { it.value >= 0 }.associate {
|
MalStatusType.entries.filter { it.value >= 0 }.associate {
|
||||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -573,7 +576,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
).text
|
).text
|
||||||
val values = parseJson<MalRoot>(res)
|
val values = parseJson<MalRoot>(res)
|
||||||
val titles =
|
val titles =
|
||||||
values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) }
|
values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) }
|
||||||
for (t in titles) {
|
for (t in titles) {
|
||||||
allTitles[t.id] = t
|
allTitles[t.id] = t
|
||||||
}
|
}
|
||||||
|
@ -582,11 +585,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
|
private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
|
||||||
// No time remaining if the show has already ended
|
// No time remaining if the show has already ended
|
||||||
try {
|
try {
|
||||||
endDate?.let {
|
endDate?.let {
|
||||||
if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
|
if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it)
|
||||||
|
?.before(Date.from(Instant.now())) != false
|
||||||
|
) return@convertJapanTimeToTimeRemaining null
|
||||||
}
|
}
|
||||||
} catch (e: ParseException) {
|
} catch (e: ParseException) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -603,7 +608,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
|
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
|
||||||
val currentYear = currentDate.get(Calendar.YEAR)
|
val currentYear = currentDate.get(Calendar.YEAR)
|
||||||
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
|
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault())
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
|
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
|
||||||
val parsedDate =
|
val parsedDate =
|
||||||
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
|
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
|
||||||
|
@ -647,13 +652,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
id: Int,
|
id: Int,
|
||||||
status: MalStatusType? = null,
|
status: MalStatusType? = null,
|
||||||
score: Int? = null,
|
score: Int? = null,
|
||||||
num_watched_episodes: Int? = null,
|
numWatchedEpisodes: Int? = null,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val res = setScoreRequest(
|
val res = setScoreRequest(
|
||||||
id,
|
id,
|
||||||
if (status == null) null else malStatusAsString[maxOf(0, status.value)],
|
if (status == null) null else malStatusAsString[maxOf(0, status.value)],
|
||||||
score,
|
score,
|
||||||
num_watched_episodes
|
numWatchedEpisodes
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (res.isNullOrBlank()) {
|
return if (res.isNullOrBlank()) {
|
||||||
|
@ -670,17 +675,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
private suspend fun setScoreRequest(
|
private suspend fun setScoreRequest(
|
||||||
id: Int,
|
id: Int,
|
||||||
status: String? = null,
|
status: String? = null,
|
||||||
score: Int? = null,
|
score: Int? = null,
|
||||||
num_watched_episodes: Int? = null,
|
numWatchedEpisodes: Int? = null,
|
||||||
): String? {
|
): String? {
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
"status" to status,
|
"status" to status,
|
||||||
"score" to score?.toString(),
|
"score" to score?.toString(),
|
||||||
"num_watched_episodes" to num_watched_episodes?.toString()
|
"num_watched_episodes" to numWatchedEpisodes?.toString()
|
||||||
).filter { it.value != null } as Map<String, String>
|
).filterValues { it != null } as Map<String, String>
|
||||||
|
|
||||||
return app.put(
|
return app.put(
|
||||||
"$apiUrl/v2/anime/$id/my_list_status",
|
"$apiUrl/v2/anime/$id/my_list_status",
|
||||||
|
@ -693,10 +699,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
|
|
||||||
data class ResponseToken(
|
data class ResponseToken(
|
||||||
@JsonProperty("token_type") val token_type: String,
|
@JsonProperty("token_type") val tokenType: String,
|
||||||
@JsonProperty("expires_in") val expires_in: Int,
|
@JsonProperty("expires_in") val expiresIn: Int,
|
||||||
@JsonProperty("access_token") val access_token: String,
|
@JsonProperty("access_token") val accessToken: String,
|
||||||
@JsonProperty("refresh_token") val refresh_token: String,
|
@JsonProperty("refresh_token") val refreshToken: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MalRoot(
|
data class MalRoot(
|
||||||
|
@ -705,7 +711,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
data class MalDatum(
|
data class MalDatum(
|
||||||
@JsonProperty("node") val node: MalNode,
|
@JsonProperty("node") val node: MalNode,
|
||||||
@JsonProperty("list_status") val list_status: MalStatus,
|
@JsonProperty("list_status") val listStatus: MalStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MalNode(
|
data class MalNode(
|
||||||
|
@ -722,16 +728,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
data class MalStatus(
|
data class MalStatus(
|
||||||
@JsonProperty("status") val status: String,
|
@JsonProperty("status") val status: String,
|
||||||
@JsonProperty("score") val score: Int,
|
@JsonProperty("score") val score: Int,
|
||||||
@JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
|
@JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
|
||||||
@JsonProperty("is_rewatching") val is_rewatching: Boolean,
|
@JsonProperty("is_rewatching") val isRewatching: Boolean,
|
||||||
@JsonProperty("updated_at") val updated_at: String,
|
@JsonProperty("updated_at") val updatedAt: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MalUser(
|
data class MalUser(
|
||||||
@JsonProperty("id") val id: Int,
|
@JsonProperty("id") val id: Int,
|
||||||
@JsonProperty("name") val name: String,
|
@JsonProperty("name") val name: String,
|
||||||
@JsonProperty("location") val location: String,
|
@JsonProperty("location") val location: String,
|
||||||
@JsonProperty("joined_at") val joined_at: String,
|
@JsonProperty("joined_at") val joinedAt: String,
|
||||||
@JsonProperty("picture") val picture: String?,
|
@JsonProperty("picture") val picture: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -744,9 +750,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
data class SmallMalAnime(
|
data class SmallMalAnime(
|
||||||
@JsonProperty("id") val id: Int,
|
@JsonProperty("id") val id: Int,
|
||||||
@JsonProperty("title") val title: String?,
|
@JsonProperty("title") val title: String?,
|
||||||
@JsonProperty("num_episodes") val num_episodes: Int,
|
@JsonProperty("num_episodes") val numEpisodes: Int,
|
||||||
@JsonProperty("my_list_status") val my_list_status: MalStatus?,
|
@JsonProperty("my_list_status") val myListStatus: MalStatus?,
|
||||||
@JsonProperty("main_picture") val main_picture: MalMainPicture?,
|
@JsonProperty("main_picture") val mainPicture: MalMainPicture?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MalSearchNode(
|
data class MalSearchNode(
|
||||||
|
|
|
@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
|
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
|
||||||
import com.lagradost.cloudstream3.utils.AppContextUtils
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
@ -30,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
|
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
|
||||||
const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
|
const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
|
||||||
const val host = "https://api.opensubtitles.com/api/v1"
|
const val HOST = "https://api.opensubtitles.com/api/v1"
|
||||||
const val TAG = "OPENSUBS"
|
const val TAG = "OPENSUBS"
|
||||||
const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms
|
const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms
|
||||||
var currentCoolDown: Long = 0L
|
var currentCoolDown: Long = 0L
|
||||||
var currentSession: SubtitleOAuthEntity? = null
|
var currentSession: SubtitleOAuthEntity? = null
|
||||||
}
|
}
|
||||||
|
@ -49,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
chain.request().newBuilder()
|
chain.request().newBuilder()
|
||||||
.removeHeader("user-agent")
|
.removeHeader("user-agent")
|
||||||
.addHeader("user-agent", userAgent)
|
.addHeader("user-agent", userAgent)
|
||||||
.addHeader("Api-Key", apiKey)
|
.addHeader("Api-Key", API_KEY)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun throwGotTooManyRequests() {
|
private fun throwGotTooManyRequests() {
|
||||||
currentCoolDown = unixTimeMs + coolDownDuration
|
currentCoolDown = unixTimeMs + COOLDOWN_DURATION
|
||||||
throw ErrorLoadingException("Too many requests")
|
throw ErrorLoadingException("Too many requests")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +114,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
private suspend fun initLogin(username: String, password: String): Boolean {
|
private suspend fun initLogin(username: String, password: String): Boolean {
|
||||||
//Log.i(TAG, "DATA = [$username] [$password]")
|
//Log.i(TAG, "DATA = [$username] [$password]")
|
||||||
val response = app.post(
|
val response = app.post(
|
||||||
url = "$host/login",
|
url = "$HOST/login",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Content-Type" to "application/json",
|
"Content-Type" to "application/json",
|
||||||
),
|
),
|
||||||
|
@ -134,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
SubtitleOAuthEntity(
|
SubtitleOAuthEntity(
|
||||||
user = username,
|
user = username,
|
||||||
pass = password,
|
pass = password,
|
||||||
access_token = token.token ?: run {
|
accessToken = token.token ?: run {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -197,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
|
|
||||||
val searchQueryUrl = when (imdbId > 0) {
|
val searchQueryUrl = when (imdbId > 0) {
|
||||||
//Use imdb_id to search if its valid
|
//Use imdb_id to search if its valid
|
||||||
true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
||||||
false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
||||||
}
|
}
|
||||||
|
|
||||||
val req = app.get(
|
val req = app.get(
|
||||||
|
@ -233,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
||||||
val year = featureDetails?.year ?: query.year
|
val year = featureDetails?.year ?: query.year
|
||||||
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
|
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
|
||||||
val isHearingImpaired = attr.hearing_impaired ?: false
|
val isHearingImpaired = attr.hearingImpaired ?: false
|
||||||
//Log.i(TAG, "Result id/name => ${item.id} / $name")
|
//Log.i(TAG, "Result id/name => ${item.id} / $name")
|
||||||
item.attributes?.files?.forEach { file ->
|
item.attributes?.files?.forEach { file ->
|
||||||
val resultData = file.fileId?.toString() ?: ""
|
val resultData = file.fileId?.toString() ?: ""
|
||||||
|
@ -266,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
throwIfCantDoRequest()
|
throwIfCantDoRequest()
|
||||||
|
|
||||||
val req = app.post(
|
val req = app.post(
|
||||||
url = "$host/download",
|
url = "$HOST/download",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
Pair(
|
Pair(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
"Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
|
"Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}"
|
||||||
),
|
),
|
||||||
Pair("Content-Type", "application/json"),
|
Pair("Content-Type", "application/json"),
|
||||||
Pair("Accept", "*/*")
|
Pair("Accept", "*/*")
|
||||||
|
@ -299,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
data class SubtitleOAuthEntity(
|
data class SubtitleOAuthEntity(
|
||||||
var user: String,
|
var user: String,
|
||||||
var pass: String,
|
var pass: String,
|
||||||
var access_token: String,
|
var accessToken: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OAuthToken(
|
data class OAuthToken(
|
||||||
|
@ -324,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
@JsonProperty("url") var url: String? = null,
|
@JsonProperty("url") var url: String? = null,
|
||||||
@JsonProperty("files") var files: List<ResultFiles>? = listOf(),
|
@JsonProperty("files") var files: List<ResultFiles>? = listOf(),
|
||||||
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(),
|
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(),
|
||||||
@JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null,
|
@JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ResultFiles(
|
data class ResultFiles(
|
||||||
|
|
|
@ -38,6 +38,7 @@ import java.security.SecureRandom
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
@ -144,8 +145,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID
|
private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID
|
||||||
private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET
|
private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET
|
||||||
private var lastLoginState = ""
|
private var lastLoginState = ""
|
||||||
|
|
||||||
const val SIMKL_TOKEN_KEY: String = "simkl_token"
|
const val SIMKL_TOKEN_KEY: String = "simkl_token"
|
||||||
|
@ -154,10 +155,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time"
|
const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time"
|
||||||
|
|
||||||
/** 2014-09-01T09:10:11Z -> 1409562611 */
|
/** 2014-09-01T09:10:11Z -> 1409562611 */
|
||||||
private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||||
fun getUnixTime(string: String?): Long? {
|
fun getUnixTime(string: String?): Long? {
|
||||||
return try {
|
return try {
|
||||||
SimpleDateFormat(simklDateFormat).apply {
|
SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
|
||||||
this.timeZone = TimeZone.getTimeZone("UTC")
|
this.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
}.parse(
|
}.parse(
|
||||||
string ?: return null
|
string ?: return null
|
||||||
|
@ -171,7 +172,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
/** 1409562611 -> 2014-09-01T09:10:11Z */
|
/** 1409562611 -> 2014-09-01T09:10:11Z */
|
||||||
fun getDateTime(unixTime: Long?): String? {
|
fun getDateTime(unixTime: Long?): String? {
|
||||||
return try {
|
return try {
|
||||||
SimpleDateFormat(simklDateFormat).apply {
|
SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
|
||||||
this.timeZone = TimeZone.getTimeZone("UTC")
|
this.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
}.format(
|
}.format(
|
||||||
Date.from(
|
Date.from(
|
||||||
|
@ -208,7 +209,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromString(string: String): SimklListStatusType? {
|
fun fromString(string: String): SimklListStatusType? {
|
||||||
return SimklListStatusType.values().firstOrNull {
|
return SimklListStatusType.entries.firstOrNull {
|
||||||
it.originalName == string
|
it.originalName == string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,17 +220,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
data class TokenRequest(
|
data class TokenRequest(
|
||||||
@JsonProperty("code") val code: String,
|
@JsonProperty("code") val code: String,
|
||||||
@JsonProperty("client_id") val client_id: String = clientId,
|
@JsonProperty("client_id") val clientId: String = CLIENT_ID,
|
||||||
@JsonProperty("client_secret") val client_secret: String = clientSecret,
|
@JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET,
|
||||||
@JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl",
|
@JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl",
|
||||||
@JsonProperty("grant_type") val grant_type: String = "authorization_code"
|
@JsonProperty("grant_type") val grantType: String = "authorization_code"
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TokenResponse(
|
data class TokenResponse(
|
||||||
/** No expiration date */
|
/** No expiration date */
|
||||||
val access_token: String,
|
@JsonProperty("access_token") val accessToken: String,
|
||||||
val token_type: String,
|
@JsonProperty("token_type") val tokenType: String,
|
||||||
val scope: String
|
@JsonProperty("scope") val scope: String
|
||||||
)
|
)
|
||||||
// -------------------
|
// -------------------
|
||||||
|
|
||||||
|
@ -261,15 +262,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
// -------------------
|
// -------------------
|
||||||
data class ActivitiesResponse(
|
data class ActivitiesResponse(
|
||||||
val all: String?,
|
@JsonProperty("all") val all: String?,
|
||||||
val tv_shows: UpdatedAt,
|
@JsonProperty("tv_shows") val tvShows: UpdatedAt,
|
||||||
val anime: UpdatedAt,
|
@JsonProperty("anime") val anime: UpdatedAt,
|
||||||
val movies: UpdatedAt,
|
@JsonProperty("movies") val movies: UpdatedAt,
|
||||||
) {
|
) {
|
||||||
data class UpdatedAt(
|
data class UpdatedAt(
|
||||||
val all: String?,
|
@JsonProperty("all") val all: String?,
|
||||||
val removed_from_list: String?,
|
@JsonProperty("removed_from_list") val removedFromList: String?,
|
||||||
val rated_at: String?,
|
@JsonProperty("rated_at") val ratedAt: String?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,7 +309,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("title") val title: String?,
|
@JsonProperty("title") val title: String?,
|
||||||
@JsonProperty("year") val year: Int?,
|
@JsonProperty("year") val year: Int?,
|
||||||
@JsonProperty("ids") val ids: Ids?,
|
@JsonProperty("ids") val ids: Ids?,
|
||||||
@JsonProperty("total_episodes") val total_episodes: Int? = null,
|
@JsonProperty("total_episodes") val totalEpisodes: Int? = null,
|
||||||
@JsonProperty("status") val status: String? = null,
|
@JsonProperty("status") val status: String? = null,
|
||||||
@JsonProperty("poster") val poster: String? = null,
|
@JsonProperty("poster") val poster: String? = null,
|
||||||
@JsonProperty("type") val type: String? = null,
|
@JsonProperty("type") val type: String? = null,
|
||||||
|
@ -540,7 +541,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint { "Requesting episodes from $url" }
|
debugPrint { "Requesting episodes from $url" }
|
||||||
return app.get(url, params = mapOf("client_id" to clientId))
|
return app.get(url, params = mapOf("client_id" to CLIENT_ID))
|
||||||
.parsedSafe<Array<EpisodeMetadata>>()?.also {
|
.parsedSafe<Array<EpisodeMetadata>>()?.also {
|
||||||
val cacheTime =
|
val cacheTime =
|
||||||
if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
|
if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
|
||||||
|
@ -558,7 +559,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("seasons") seasons: List<Season>? = null,
|
@JsonProperty("seasons") seasons: List<Season>? = null,
|
||||||
@JsonProperty("episodes") episodes: List<Season.Episode>? = null,
|
@JsonProperty("episodes") episodes: List<Season.Episode>? = null,
|
||||||
@JsonProperty("rating") val rating: Int? = null,
|
@JsonProperty("rating") val rating: Int? = null,
|
||||||
@JsonProperty("rated_at") val rated_at: String? = null,
|
@JsonProperty("rated_at") val ratedAt: String? = null,
|
||||||
) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes)
|
) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes)
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
@ -567,7 +568,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("year") year: Int?,
|
@JsonProperty("year") year: Int?,
|
||||||
@JsonProperty("ids") ids: Ids?,
|
@JsonProperty("ids") ids: Ids?,
|
||||||
@JsonProperty("rating") val rating: Int,
|
@JsonProperty("rating") val rating: Int,
|
||||||
@JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime)
|
@JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime)
|
||||||
) : MediaObject(title, year, ids)
|
) : MediaObject(title, year, ids)
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
@ -576,7 +577,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("year") year: Int?,
|
@JsonProperty("year") year: Int?,
|
||||||
@JsonProperty("ids") ids: Ids?,
|
@JsonProperty("ids") ids: Ids?,
|
||||||
@JsonProperty("to") val to: String,
|
@JsonProperty("to") val to: String,
|
||||||
@JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime)
|
@JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime)
|
||||||
) : MediaObject(title, year, ids)
|
) : MediaObject(title, year, ids)
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
@ -631,24 +632,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Metadata {
|
interface Metadata {
|
||||||
val last_watched_at: String?
|
val lastWatchedAt: String?
|
||||||
val status: String?
|
val status: String?
|
||||||
val user_rating: Int?
|
val userRating: Int?
|
||||||
val last_watched: String?
|
val lastWatched: String?
|
||||||
val watched_episodes_count: Int?
|
val watchedEpisodesCount: Int?
|
||||||
val total_episodes_count: Int?
|
val totalEpisodesCount: Int?
|
||||||
|
|
||||||
fun getIds(): ShowMetadata.Show.Ids
|
fun getIds(): ShowMetadata.Show.Ids
|
||||||
fun toLibraryItem(): SyncAPI.LibraryItem
|
fun toLibraryItem(): SyncAPI.LibraryItem
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MovieMetadata(
|
data class MovieMetadata(
|
||||||
override val last_watched_at: String?,
|
@JsonProperty("last_watched_at") override val lastWatchedAt: String?,
|
||||||
override val status: String,
|
@JsonProperty("status") override val status: String,
|
||||||
override val user_rating: Int?,
|
@JsonProperty("user_rating") override val userRating: Int?,
|
||||||
override val last_watched: String?,
|
@JsonProperty("last_watched") override val lastWatched: String?,
|
||||||
override val watched_episodes_count: Int?,
|
@JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?,
|
||||||
override val total_episodes_count: Int?,
|
@JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?,
|
||||||
val movie: ShowMetadata.Show
|
val movie: ShowMetadata.Show
|
||||||
) : Metadata {
|
) : Metadata {
|
||||||
override fun getIds(): ShowMetadata.Show.Ids {
|
override fun getIds(): ShowMetadata.Show.Ids {
|
||||||
|
@ -660,10 +661,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
this.movie.title,
|
this.movie.title,
|
||||||
"https://simkl.com/tv/${movie.ids.simkl}",
|
"https://simkl.com/tv/${movie.ids.simkl}",
|
||||||
movie.ids.simkl.toString(),
|
movie.ids.simkl.toString(),
|
||||||
this.watched_episodes_count,
|
this.watchedEpisodesCount,
|
||||||
this.total_episodes_count,
|
this.totalEpisodesCount,
|
||||||
this.user_rating?.times(10),
|
this.userRating?.times(10),
|
||||||
getUnixTime(last_watched_at) ?: 0,
|
getUnixTime(lastWatchedAt) ?: 0,
|
||||||
"Simkl",
|
"Simkl",
|
||||||
TvType.Movie,
|
TvType.Movie,
|
||||||
this.movie.poster?.let { getPosterUrl(it) },
|
this.movie.poster?.let { getPosterUrl(it) },
|
||||||
|
@ -675,12 +676,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ShowMetadata(
|
data class ShowMetadata(
|
||||||
@JsonProperty("last_watched_at") override val last_watched_at: String?,
|
@JsonProperty("last_watched_at") override val lastWatchedAt: String?,
|
||||||
@JsonProperty("status") override val status: String,
|
@JsonProperty("status") override val status: String,
|
||||||
@JsonProperty("user_rating") override val user_rating: Int?,
|
@JsonProperty("user_rating") override val userRating: Int?,
|
||||||
@JsonProperty("last_watched") override val last_watched: String?,
|
@JsonProperty("last_watched") override val lastWatched: String?,
|
||||||
@JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?,
|
@JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?,
|
||||||
@JsonProperty("total_episodes_count") override val total_episodes_count: Int?,
|
@JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?,
|
||||||
@JsonProperty("show") val show: Show
|
@JsonProperty("show") val show: Show
|
||||||
) : Metadata {
|
) : Metadata {
|
||||||
override fun getIds(): Show.Ids {
|
override fun getIds(): Show.Ids {
|
||||||
|
@ -692,10 +693,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
this.show.title,
|
this.show.title,
|
||||||
"https://simkl.com/tv/${show.ids.simkl}",
|
"https://simkl.com/tv/${show.ids.simkl}",
|
||||||
show.ids.simkl.toString(),
|
show.ids.simkl.toString(),
|
||||||
this.watched_episodes_count,
|
this.watchedEpisodesCount,
|
||||||
this.total_episodes_count,
|
this.totalEpisodesCount,
|
||||||
this.user_rating?.times(10),
|
this.userRating?.times(10),
|
||||||
getUnixTime(last_watched_at) ?: 0,
|
getUnixTime(lastWatchedAt) ?: 0,
|
||||||
"Simkl",
|
"Simkl",
|
||||||
TvType.Anime,
|
TvType.Anime,
|
||||||
this.show.poster?.let { getPosterUrl(it) },
|
this.show.poster?.let { getPosterUrl(it) },
|
||||||
|
@ -749,7 +750,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
chain.request()
|
chain.request()
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer $token")
|
.addHeader("Authorization", "Bearer $token")
|
||||||
.addHeader("simkl-api-key", clientId)
|
.addHeader("simkl-api-key", CLIENT_ID)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -810,7 +811,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val episodeConstructor = SimklEpisodeConstructor(
|
val episodeConstructor = SimklEpisodeConstructor(
|
||||||
searchResult.ids?.simkl,
|
searchResult.ids?.simkl,
|
||||||
searchResult.type,
|
searchResult.type,
|
||||||
searchResult.total_episodes,
|
searchResult.totalEpisodes,
|
||||||
searchResult.hasEnded()
|
searchResult.hasEnded()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -832,12 +833,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
?: return null,
|
?: return null,
|
||||||
score = foundItem.user_rating,
|
score = foundItem.userRating,
|
||||||
watchedEpisodes = foundItem.watched_episodes_count,
|
watchedEpisodes = foundItem.watchedEpisodesCount,
|
||||||
maxEpisodes = searchResult.total_episodes,
|
maxEpisodes = searchResult.totalEpisodes,
|
||||||
episodeConstructor = episodeConstructor,
|
episodeConstructor = episodeConstructor,
|
||||||
oldEpisodes = foundItem.watched_episodes_count ?: 0,
|
oldEpisodes = foundItem.watchedEpisodesCount ?: 0,
|
||||||
oldScore = foundItem.user_rating,
|
oldScore = foundItem.userRating,
|
||||||
oldStatus = foundItem.status
|
oldStatus = foundItem.status
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -845,7 +846,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
status = SyncWatchType.fromInternalId(SimklListStatusType.None.value),
|
status = SyncWatchType.fromInternalId(SimklListStatusType.None.value),
|
||||||
score = 0,
|
score = 0,
|
||||||
watchedEpisodes = 0,
|
watchedEpisodes = 0,
|
||||||
maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes,
|
maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes,
|
||||||
episodeConstructor = episodeConstructor,
|
episodeConstructor = episodeConstructor,
|
||||||
oldEpisodes = 0,
|
oldEpisodes = 0,
|
||||||
oldStatus = null,
|
oldStatus = null,
|
||||||
|
@ -891,12 +892,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
|
|
||||||
/** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */
|
/** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */
|
||||||
suspend fun searchByIds(serviceMap: Map<SimklSyncServices, String>): Array<MediaObject>? {
|
private suspend fun searchByIds(serviceMap: Map<SimklSyncServices, String>): Array<MediaObject>? {
|
||||||
if (serviceMap.isEmpty()) return emptyArray()
|
if (serviceMap.isEmpty()) return emptyArray()
|
||||||
|
|
||||||
return app.get(
|
return app.get(
|
||||||
"$mainUrl/search/id",
|
"$mainUrl/search/id",
|
||||||
params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) ->
|
params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) ->
|
||||||
service.originalName to id
|
service.originalName to id
|
||||||
}
|
}
|
||||||
).parsedSafe()
|
).parsedSafe()
|
||||||
|
@ -904,14 +905,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
||||||
return app.get(
|
return app.get(
|
||||||
"$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name)
|
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name)
|
||||||
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
|
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun authenticate(activity: FragmentActivity?) {
|
override fun authenticate(activity: FragmentActivity?) {
|
||||||
lastLoginState = BigInteger(130, SecureRandom()).toString(32)
|
lastLoginState = BigInteger(130, SecureRandom()).toString(32)
|
||||||
val url =
|
val url =
|
||||||
"https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState"
|
"https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}&state=$lastLoginState"
|
||||||
openBrowser(url, activity)
|
openBrowser(url, activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -961,15 +962,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val activities = getActivities()
|
val activities = getActivities()
|
||||||
val lastCacheUpdate = getKey<Long>(accountId, SIMKL_CACHED_LIST_TIME)
|
val lastCacheUpdate = getKey<Long>(accountId, SIMKL_CACHED_LIST_TIME)
|
||||||
val lastRemoval = listOf(
|
val lastRemoval = listOf(
|
||||||
activities?.tv_shows?.removed_from_list,
|
activities?.tvShows?.removedFromList,
|
||||||
activities?.anime?.removed_from_list,
|
activities?.anime?.removedFromList,
|
||||||
activities?.movies?.removed_from_list
|
activities?.movies?.removedFromList
|
||||||
).maxOf {
|
).maxOf {
|
||||||
getUnixTime(it) ?: -1
|
getUnixTime(it) ?: -1
|
||||||
}
|
}
|
||||||
val lastRealUpdate =
|
val lastRealUpdate =
|
||||||
listOf(
|
listOf(
|
||||||
activities?.tv_shows?.all,
|
activities?.tvShows?.all,
|
||||||
activities?.anime?.all,
|
activities?.anime?.all,
|
||||||
activities?.movies?.all,
|
activities?.movies?.all,
|
||||||
).maxOf {
|
).maxOf {
|
||||||
|
@ -1039,7 +1040,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun getDevicePin(): OAuth2API.PinAuthData? {
|
override suspend fun getDevicePin(): OAuth2API.PinAuthData? {
|
||||||
val pinAuthResp = app.get(
|
val pinAuthResp = app.get(
|
||||||
"$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}"
|
"$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}"
|
||||||
).parsedSafe<PinAuthResponse>() ?: return null
|
).parsedSafe<PinAuthResponse>() ?: return null
|
||||||
|
|
||||||
return OAuth2API.PinAuthData(
|
return OAuth2API.PinAuthData(
|
||||||
|
@ -1053,7 +1054,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean {
|
override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean {
|
||||||
val pinAuthResp = app.get(
|
val pinAuthResp = app.get(
|
||||||
"$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId"
|
"$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$CLIENT_ID"
|
||||||
).parsedSafe<PinExchangeResponse>() ?: return false
|
).parsedSafe<PinExchangeResponse>() ?: return false
|
||||||
|
|
||||||
if (pinAuthResp.accessToken != null) {
|
if (pinAuthResp.accessToken != null) {
|
||||||
|
@ -1088,7 +1089,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
).parsedSafe<TokenResponse>() ?: return false
|
).parsedSafe<TokenResponse>() ?: return false
|
||||||
|
|
||||||
switchToNewAccount()
|
switchToNewAccount()
|
||||||
setKey(accountId, SIMKL_TOKEN_KEY, token.access_token)
|
setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken)
|
||||||
|
|
||||||
val user = getUser()
|
val user = getUser()
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
|
|
@ -59,6 +59,7 @@ class SubSourceApi : AbstractSubProvider {
|
||||||
it?.subs?.filter { sub ->
|
it?.subs?.filter { sub ->
|
||||||
sub.releaseName!!.contains(
|
sub.releaseName!!.contains(
|
||||||
String.format(
|
String.format(
|
||||||
|
null,
|
||||||
"E%02d",
|
"E%02d",
|
||||||
query.epNumber
|
query.epNumber
|
||||||
)
|
)
|
||||||
|
|
|
@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) {
|
||||||
|
|
||||||
private val cache = threadSafeListOf<SavedLoadResponse>()
|
private val cache = threadSafeListOf<SavedLoadResponse>()
|
||||||
private var cacheIndex: Int = 0
|
private var cacheIndex: Int = 0
|
||||||
const val cacheSize = 20
|
const val CACHE_SIZE = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun afterPluginsLoaded(forceReload: Boolean) {
|
private fun afterPluginsLoaded(forceReload: Boolean) {
|
||||||
|
@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) {
|
||||||
val add = SavedLoadResponse(unixTime, response, lookingForHash)
|
val add = SavedLoadResponse(unixTime, response, lookingForHash)
|
||||||
|
|
||||||
synchronized(cache) {
|
synchronized(cache) {
|
||||||
if (cache.size > cacheSize) {
|
if (cache.size > CACHE_SIZE) {
|
||||||
cache[cacheIndex] = add // rolling cache
|
cache[cacheIndex] = add // rolling cache
|
||||||
cacheIndex = (cacheIndex + 1) % cacheSize
|
cacheIndex = (cacheIndex + 1) % CACHE_SIZE
|
||||||
} else {
|
} else {
|
||||||
cache.add(add)
|
cache.add(add)
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ abstract class BaseAdapter<
|
||||||
holder.onViewDetachedFromWindow()
|
holder.onViewDetachedFromWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun save(recyclerView: RecyclerView) {
|
fun save(recyclerView: RecyclerView) {
|
||||||
for (child in recyclerView.children) {
|
for (child in recyclerView.children) {
|
||||||
val holder =
|
val holder =
|
||||||
|
@ -124,6 +125,7 @@ abstract class BaseAdapter<
|
||||||
stateViewModel.layoutManagerStates[id]?.clear()
|
stateViewModel.layoutManagerStates[id]?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
private fun getState(holder: ViewHolderState<S>): S? =
|
private fun getState(holder: ViewHolderState<S>): S? =
|
||||||
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
|
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.view.Menu
|
||||||
import android.view.View.*
|
import android.view.View.*
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.kotlinModule
|
import com.fasterxml.jackson.module.kotlin.kotlinModule
|
||||||
|
@ -263,6 +264,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
||||||
|
|
||||||
var isLoadingMore = false
|
var isLoadingMore = false
|
||||||
|
|
||||||
|
|
||||||
override fun onMediaStatusUpdated() {
|
override fun onMediaStatusUpdated() {
|
||||||
super.onMediaStatusUpdated()
|
super.onMediaStatusUpdated()
|
||||||
val meta = getCurrentMetaData()
|
val meta = getCurrentMetaData()
|
||||||
|
|
|
@ -8,8 +8,8 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
class GrdLayoutManager(val context: Context, spanCount: Int) :
|
||||||
GridLayoutManager(context, _spanCount) {
|
GridLayoutManager(context, spanCount) {
|
||||||
override fun onFocusSearchFailed(
|
override fun onFocusSearchFailed(
|
||||||
focused: View,
|
focused: View,
|
||||||
focusDirection: Int,
|
focusDirection: Int,
|
||||||
|
|
|
@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() {
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT)
|
FrameLayout.LayoutParams.WRAP_CONTENT)
|
||||||
binding.frame.addView(newStar)
|
binding.frame.addView(newStar)
|
||||||
|
|
||||||
newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX
|
newStar.scaleX += Math.random().toFloat() * 1.5f
|
||||||
newStar.scaleY = newStar.scaleX
|
newStar.scaleY = newStar.scaleX
|
||||||
starW *= newStar.scaleX
|
starW *= newStar.scaleX
|
||||||
starH *= newStar.scaleY
|
starH *= newStar.scaleY
|
||||||
|
|
|
@ -15,7 +15,7 @@ open class NonFinalAdapterListUpdateCallback
|
||||||
/**
|
/**
|
||||||
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
|
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
|
||||||
*
|
*
|
||||||
* @param adapter The Adapter to send updates to.
|
* @param mAdapter The Adapter to send updates to.
|
||||||
*/(private var mAdapter: RecyclerView.Adapter<*>) :
|
*/(private var mAdapter: RecyclerView.Adapter<*>) :
|
||||||
ListUpdateCallback {
|
ListUpdateCallback {
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab
|
||||||
NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24);
|
NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
|
fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,6 @@ enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @Dr
|
||||||
REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24);
|
REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
|
fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,10 @@ import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.lagradost.cloudstream3.MainActivity
|
import com.lagradost.cloudstream3.MainActivity
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
|
@ -29,6 +31,7 @@ class WebviewFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding?.webView?.webViewClient = object : WebViewClient() {
|
binding?.webView?.webViewClient = object : WebViewClient() {
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun shouldOverrideUrlLoading(
|
override fun shouldOverrideUrlLoading(
|
||||||
view: WebView?,
|
view: WebView?,
|
||||||
request: WebResourceRequest?
|
request: WebResourceRequest?
|
||||||
|
|
|
@ -54,6 +54,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@Suppress("LeakingThis")
|
||||||
resetViewData()
|
resetViewData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
class DownloadButton(context: Context, attributeSet: AttributeSet) :
|
class DownloadButton(context: Context, attributeSet: AttributeSet) :
|
||||||
PieFetchButton(context, attributeSet) {
|
PieFetchButton(context, attributeSet) {
|
||||||
|
|
||||||
var mainText: TextView? = null
|
private var mainText: TextView? = null
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
progressText = findViewById(R.id.result_movie_download_text_precentage)
|
progressText = findViewById(R.id.result_movie_download_text_precentage)
|
||||||
|
|
|
@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
|
import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
|
|
||||||
class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) {
|
class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) {
|
||||||
|
@ -54,7 +54,7 @@ class HomeChildItemAdapter(
|
||||||
var hasNext: Boolean = false
|
var hasNext: Boolean = false
|
||||||
|
|
||||||
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> {
|
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> {
|
||||||
val expanded = parent.context.IsBottomLayout()
|
val expanded = parent.context.isBottomLayout()
|
||||||
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
||||||
|
|
||||||
val root = LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
val root = LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||||
|
@ -133,7 +133,6 @@ class HomeChildItemAdapter(
|
||||||
item,
|
item,
|
||||||
position,
|
position,
|
||||||
holder.itemView,
|
holder.itemView,
|
||||||
null, // nextFocusBehavior,
|
|
||||||
nextFocusUp,
|
nextFocusUp,
|
||||||
nextFocusDown
|
nextFocusDown
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,7 +17,6 @@ import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.*
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
@ -234,7 +233,7 @@ class HomeFragment : Fragment() {
|
||||||
return bottomSheetDialogBuilder
|
return bottomSheetDialogBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPairList(
|
private fun getPairList(
|
||||||
anime: Chip?,
|
anime: Chip?,
|
||||||
cartoons: Chip?,
|
cartoons: Chip?,
|
||||||
tvs: Chip?,
|
tvs: Chip?,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package com.lagradost.cloudstream3.ui.home
|
package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -53,12 +55,12 @@ open class ParentItemAdapter(
|
||||||
"value",
|
"value",
|
||||||
recyclerView?.layoutManager?.onSaveInstanceState()
|
recyclerView?.layoutManager?.onSaveInstanceState()
|
||||||
)
|
)
|
||||||
(recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView)
|
(recyclerView?.adapter as? BaseAdapter<*, *>)?.save(recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun restore(state: Bundle) {
|
override fun restore(state: Bundle) {
|
||||||
(binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState(
|
(binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState(
|
||||||
state.getParcelable("value")
|
state.getSafeParcelable<Parcelable>("value")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,4 +171,9 @@ open class ParentItemAdapter(
|
||||||
submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
|
submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
|
||||||
.toMutableList())
|
.toMutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
inline fun <reified T> Bundle.getSafeParcelable(key: String): T? =
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelable(key)
|
||||||
|
else getParcelable(key, T::class.java)
|
|
@ -117,15 +117,12 @@ class HomeParentItemAdapterPreview(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun restore(state: Bundle) {
|
override fun restore(state: Bundle) {
|
||||||
state.getParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
|
state.getSafeParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
|
||||||
resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
||||||
}
|
}
|
||||||
state.getParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
|
state.getSafeParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
|
||||||
bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
||||||
}
|
}
|
||||||
//state.getInt("previewViewpager").let { recycle ->
|
|
||||||
// previewViewpager.setCurrentItem(recycle,true)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val previewAdapter = HomeScrollAdapter(fragment = fragment)
|
val previewAdapter = HomeScrollAdapter(fragment = fragment)
|
||||||
|
|
|
@ -152,7 +152,7 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}?.distinctBy { it.first } ?: return@launchSafe
|
}?.distinctBy { it.first } ?: return@launchSafe
|
||||||
|
|
||||||
val length = WatchType.values().size
|
val length = WatchType.entries.size
|
||||||
val currentWatchTypes = mutableSetOf<WatchType>()
|
val currentWatchTypes = mutableSetOf<WatchType>()
|
||||||
|
|
||||||
for (watch in watchStatusIds) {
|
for (watch in watchStatusIds) {
|
||||||
|
@ -387,7 +387,9 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
is Resource.Failure -> {
|
is Resource.Failure -> {
|
||||||
|
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||||
_page.postValue(data!!)
|
_page.postValue(data!!)
|
||||||
|
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||||
_preview.postValue(data!!)
|
_preview.postValue(data!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,9 +399,7 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun click(callback: SearchClickCallback) {
|
fun click(callback: SearchClickCallback) {
|
||||||
if (callback.action == SEARCH_ACTION_FOCUSED) {
|
if (callback.action != SEARCH_ACTION_FOCUSED) {
|
||||||
//focusCallback(callback.card)
|
|
||||||
} else {
|
|
||||||
SearchHelper.handleSearchClickCallback(callback)
|
SearchHelper.handleSearchClickCallback(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -516,7 +516,7 @@ class HomeViewModel : ViewModel() {
|
||||||
} else {
|
} else {
|
||||||
_page.postValue(Resource.Loading())
|
_page.postValue(Resource.Loading())
|
||||||
if (preferredApiName != null)
|
if (preferredApiName != null)
|
||||||
_apiName.postValue(preferredApiName)
|
_apiName.postValue(preferredApiName!!)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if the api is found, then set it to it and save key
|
// if the api is found, then set it to it and save key
|
||||||
|
|
|
@ -600,8 +600,4 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MenuSearchView(context: Context) : SearchView(context) {
|
class MenuSearchView(context: Context) : SearchView(context)
|
||||||
override fun onActionViewCollapsed() {
|
|
||||||
super.onActionViewCollapsed()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
import com.lagradost.cloudstream3.ui.BaseAdapter
|
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||||
import com.lagradost.cloudstream3.ui.BaseDiffCallback
|
import com.lagradost.cloudstream3.ui.BaseDiffCallback
|
||||||
import com.lagradost.cloudstream3.ui.ViewHolderState
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
|
import com.lagradost.cloudstream3.ui.home.getSafeParcelable
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
@ -32,7 +33,7 @@ class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun restore(state: Bundle) {
|
override fun restore(state: Bundle) {
|
||||||
state.getParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
|
state.getSafeParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
|
||||||
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
|
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.ui.*
|
import androidx.media3.ui.*
|
||||||
|
@ -216,7 +217,7 @@ abstract class AbstractPlayerFragment(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
player.handleEvent(
|
player.handleEvent(
|
||||||
CSPlayerEvent.values()[intent.getIntExtra(
|
CSPlayerEvent.entries[intent.getIntExtra(
|
||||||
EXTRA_CONTROL_TYPE,
|
EXTRA_CONTROL_TYPE,
|
||||||
0
|
0
|
||||||
)], source = PlayerEventSource.UI
|
)], source = PlayerEventSource.UI
|
||||||
|
@ -603,12 +604,12 @@ abstract class AbstractPlayerFragment(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nextResize() {
|
fun nextResize() {
|
||||||
resizeMode = (resizeMode + 1) % PlayerResize.values().size
|
resizeMode = (resizeMode + 1) % PlayerResize.entries.size
|
||||||
resize(resizeMode, true)
|
resize(resizeMode, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resize(resize: Int, showToast: Boolean) {
|
fun resize(resize: Int, showToast: Boolean) {
|
||||||
resize(PlayerResize.values()[resize], showToast)
|
resize(PlayerResize.entries[resize], showToast)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
|
|
@ -9,7 +9,11 @@ import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Rational
|
import android.util.Rational
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.media3.common.C.*
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.C.TIME_UNSET
|
||||||
|
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||||
|
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||||
|
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
@ -19,9 +23,10 @@ import androidx.media3.common.TrackGroup
|
||||||
import androidx.media3.common.TrackSelectionOverride
|
import androidx.media3.common.TrackSelectionOverride
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.VideoSize
|
import androidx.media3.common.VideoSize
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.database.StandaloneDatabaseProvider
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
import androidx.media3.datasource.DataSource
|
import androidx.media3.datasource.DataSource
|
||||||
import androidx.media3.datasource.DefaultDataSourceFactory
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
import androidx.media3.datasource.DefaultHttpDataSource
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
import androidx.media3.datasource.HttpDataSource
|
import androidx.media3.datasource.HttpDataSource
|
||||||
import androidx.media3.datasource.cache.CacheDataSource
|
import androidx.media3.datasource.cache.CacheDataSource
|
||||||
|
@ -66,7 +71,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.IllegalArgumentException
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
|
@ -84,7 +88,7 @@ const val toleranceBeforeUs = 300_000L
|
||||||
* seek position, in microseconds. Must be non-negative.
|
* seek position, in microseconds. Must be non-negative.
|
||||||
*/
|
*/
|
||||||
const val toleranceAfterUs = 300_000L
|
const val toleranceAfterUs = 300_000L
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
class CS3IPlayer : IPlayer {
|
class CS3IPlayer : IPlayer {
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
private var exoPlayer: ExoPlayer? = null
|
private var exoPlayer: ExoPlayer? = null
|
||||||
|
@ -257,7 +261,6 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
private var currentSubtitles: SubtitleData? = null
|
private var currentSubtitles: SubtitleData? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
|
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
|
||||||
if (id == null) return null
|
if (id == null) return null
|
||||||
// This beast of an expression does:
|
// This beast of an expression does:
|
||||||
|
@ -342,7 +345,6 @@ class CS3IPlayer : IPlayer {
|
||||||
}.flatten()
|
}.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
|
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
|
||||||
return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
|
return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
|
||||||
if (this.isSupported)
|
if (this.isSupported)
|
||||||
|
@ -371,7 +373,6 @@ class CS3IPlayer : IPlayer {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun getVideoTracks(): CurrentTracks {
|
override fun getVideoTracks(): CurrentTracks {
|
||||||
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
|
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
|
||||||
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
|
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
|
||||||
|
@ -391,7 +392,6 @@ class CS3IPlayer : IPlayer {
|
||||||
/**
|
/**
|
||||||
* @return True if the player should be reloaded
|
* @return True if the player should be reloaded
|
||||||
* */
|
* */
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
|
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
|
||||||
Log.i(TAG, "setPreferredSubtitles init $subtitle")
|
Log.i(TAG, "setPreferredSubtitles init $subtitle")
|
||||||
currentSubtitles = subtitle
|
currentSubtitles = subtitle
|
||||||
|
@ -451,7 +451,7 @@ class CS3IPlayer : IPlayer {
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentSubtitleOffset: Long = 0
|
private var currentSubtitleOffset: Long = 0
|
||||||
|
|
||||||
override fun setSubtitleOffset(offset: Long) {
|
override fun setSubtitleOffset(offset: Long) {
|
||||||
currentSubtitleOffset = offset
|
currentSubtitleOffset = offset
|
||||||
|
@ -459,7 +459,7 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSubtitleOffset(): Long {
|
override fun getSubtitleOffset(): Long {
|
||||||
return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset
|
return currentSubtitleOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentPreferredSubtitle(): SubtitleData? {
|
override fun getCurrentPreferredSubtitle(): SubtitleData? {
|
||||||
|
@ -470,7 +470,6 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun getAspectRatio(): Rational? {
|
override fun getAspectRatio(): Rational? {
|
||||||
return exoPlayer?.videoFormat?.let { format ->
|
return exoPlayer?.videoFormat?.let { format ->
|
||||||
Rational(format.width, format.height)
|
Rational(format.width, format.height)
|
||||||
|
@ -481,14 +480,13 @@ class CS3IPlayer : IPlayer {
|
||||||
subtitleHelper.setSubStyle(style)
|
subtitleHelper.setSubStyle(style)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun saveData() {
|
override fun saveData() {
|
||||||
Log.i(TAG, "saveData")
|
Log.i(TAG, "saveData")
|
||||||
updatedTime()
|
updatedTime()
|
||||||
|
|
||||||
exoPlayer?.let { exo ->
|
exoPlayer?.let { exo ->
|
||||||
playbackPosition = exo.currentPosition
|
playbackPosition = exo.currentPosition
|
||||||
currentWindow = exo.currentWindowIndex
|
currentWindow = exo.currentMediaItemIndex
|
||||||
isPlaying = exo.isPlaying
|
isPlaying = exo.isPlaying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -500,7 +498,7 @@ class CS3IPlayer : IPlayer {
|
||||||
updatedTime()
|
updatedTime()
|
||||||
|
|
||||||
exoPlayer?.apply {
|
exoPlayer?.apply {
|
||||||
setPlayWhenReady(false)
|
playWhenReady = false
|
||||||
stop()
|
stop()
|
||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
|
@ -563,7 +561,6 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
var requestSubtitleUpdate: (() -> Unit)? = null
|
var requestSubtitleUpdate: (() -> Unit)? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
|
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
|
||||||
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
|
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
|
||||||
return source.apply {
|
return source.apply {
|
||||||
|
@ -571,7 +568,6 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
|
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
|
||||||
val provider = getApiFromNameNull(link.source)
|
val provider = getApiFromNameNull(link.source)
|
||||||
val interceptor = provider?.getVideoInterceptor(link)
|
val interceptor = provider?.getVideoInterceptor(link)
|
||||||
|
@ -604,53 +600,10 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun Context.createOfflineSource(): DataSource.Factory {
|
private fun Context.createOfflineSource(): DataSource.Factory {
|
||||||
return DefaultDataSourceFactory(this, USER_AGENT)
|
return DefaultDataSource.Factory(this, DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*private fun getSubSources(
|
|
||||||
onlineSourceFactory: DataSource.Factory?,
|
|
||||||
offlineSourceFactory: DataSource.Factory?,
|
|
||||||
subHelper: PlayerSubtitleHelper,
|
|
||||||
): Pair<List<SingleSampleMediaSource>, List<SubtitleData>> {
|
|
||||||
val activeSubtitles = ArrayList<SubtitleData>()
|
|
||||||
val subSources = subHelper.getAllSubtitles().mapNotNull { sub ->
|
|
||||||
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
|
|
||||||
.setMimeType(sub.mimeType)
|
|
||||||
.setLanguage("_${sub.name}")
|
|
||||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
|
||||||
.build()
|
|
||||||
when (sub.origin) {
|
|
||||||
SubtitleOrigin.DOWNLOADED_FILE -> {
|
|
||||||
if (offlineSourceFactory != null) {
|
|
||||||
activeSubtitles.add(sub)
|
|
||||||
SingleSampleMediaSource.Factory(offlineSourceFactory)
|
|
||||||
.createMediaSource(subConfig, C.TIME_UNSET)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SubtitleOrigin.URL -> {
|
|
||||||
if (onlineSourceFactory != null) {
|
|
||||||
activeSubtitles.add(sub)
|
|
||||||
SingleSampleMediaSource.Factory(onlineSourceFactory)
|
|
||||||
.createMediaSource(subConfig, C.TIME_UNSET)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SubtitleOrigin.OPEN_SUBTITLES -> {
|
|
||||||
// TODO
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println("SUBSRC: ${subSources.size} activeSubtitles : ${activeSubtitles.size} of ${subHelper.getAllSubtitles().size} ")
|
|
||||||
return Pair(subSources, activeSubtitles)
|
|
||||||
}*/
|
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
|
private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
|
||||||
return try {
|
return try {
|
||||||
val databaseProvider = StandaloneDatabaseProvider(context)
|
val databaseProvider = StandaloneDatabaseProvider(context)
|
||||||
|
@ -682,7 +635,6 @@ class CS3IPlayer : IPlayer {
|
||||||
return getMediaItemBuilder(mimeType).setUri(url).build()
|
return getMediaItemBuilder(mimeType).setUri(url).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
|
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
|
||||||
val trackSelector = DefaultTrackSelector(context)
|
val trackSelector = DefaultTrackSelector(context)
|
||||||
trackSelector.parameters = trackSelector.buildUponParameters()
|
trackSelector.parameters = trackSelector.buildUponParameters()
|
||||||
|
@ -696,7 +648,6 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
var currentTextRenderer: CustomTextRenderer? = null
|
var currentTextRenderer: CustomTextRenderer? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun buildExoPlayer(
|
private fun buildExoPlayer(
|
||||||
context: Context,
|
context: Context,
|
||||||
mediaItemSlices: List<MediaItemSlice>,
|
mediaItemSlices: List<MediaItemSlice>,
|
||||||
|
@ -736,7 +687,7 @@ class CS3IPlayer : IPlayer {
|
||||||
textRendererOutput,
|
textRendererOutput,
|
||||||
eventHandler.looper,
|
eventHandler.looper,
|
||||||
CustomSubtitleDecoderFactory()
|
CustomSubtitleDecoderFactory()
|
||||||
).also { this.currentTextRenderer = it }
|
).also { renderer -> this.currentTextRenderer = renderer }
|
||||||
currentTextRenderer
|
currentTextRenderer
|
||||||
} else it
|
} else it
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
@ -1033,7 +984,7 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
//fixme: Use onPlaybackStateChanged(int) and onPlayWhenReadyChanged(boolean, int) instead.
|
||||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||||
exoPlayer?.let { exo ->
|
exoPlayer?.let { exo ->
|
||||||
event(
|
event(
|
||||||
|
@ -1169,7 +1120,6 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
|
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
|
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
|
||||||
lastTimeStamps = timeStamps
|
lastTimeStamps = timeStamps
|
||||||
timeStamps.forEach { timestamp ->
|
timeStamps.forEach { timestamp ->
|
||||||
|
@ -1187,7 +1137,6 @@ class CS3IPlayer : IPlayer {
|
||||||
updatedTime(source = PlayerEventSource.Player)
|
updatedTime(source = PlayerEventSource.Player)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
fun onRenderFirst() {
|
fun onRenderFirst() {
|
||||||
if (hasUsedFirstRender) { // this insures that we only call this once per player load
|
if (hasUsedFirstRender) { // this insures that we only call this once per player load
|
||||||
return
|
return
|
||||||
|
@ -1254,7 +1203,6 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun getSubSources(
|
private fun getSubSources(
|
||||||
onlineSourceFactory: HttpDataSource.Factory?,
|
onlineSourceFactory: HttpDataSource.Factory?,
|
||||||
offlineSourceFactory: DataSource.Factory?,
|
offlineSourceFactory: DataSource.Factory?,
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
@ -31,7 +32,7 @@ import java.nio.charset.Charset
|
||||||
* @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not
|
* @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not
|
||||||
* enough to identify the subtitle format.
|
* enough to identify the subtitle format.
|
||||||
**/
|
**/
|
||||||
@UnstableApi
|
@OptIn(UnstableApi::class)
|
||||||
class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
|
class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
|
||||||
companion object {
|
companion object {
|
||||||
fun updateForcedEncoding(context: Context) {
|
fun updateForcedEncoding(context: Context) {
|
||||||
|
@ -72,7 +73,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
|
||||||
RegexOption.IGNORE_CASE
|
RegexOption.IGNORE_CASE
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*"""))
|
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\s]*?[])}]\s*"""))
|
||||||
|
|
||||||
//https://emptycharacter.com/
|
//https://emptycharacter.com/
|
||||||
//https://www.fileformat.info/info/unicode/char/200b/index.htm
|
//https://www.fileformat.info/info/unicode/char/200b/index.htm
|
||||||
|
@ -262,7 +263,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */
|
/** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */
|
||||||
@UnstableApi
|
@OptIn(UnstableApi::class)
|
||||||
class CustomSubtitleDecoderFactory : SubtitleDecoderFactory {
|
class CustomSubtitleDecoderFactory : SubtitleDecoderFactory {
|
||||||
override fun supportsFormat(format: Format): Boolean {
|
override fun supportsFormat(format: Format): Boolean {
|
||||||
// return SubtitleDecoderFactory.DEFAULT.supportsFormat(format)
|
// return SubtitleDecoderFactory.DEFAULT.supportsFormat(format)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.text.SubtitleDecoderFactory
|
import androidx.media3.exoplayer.text.SubtitleDecoderFactory
|
||||||
import androidx.media3.exoplayer.text.TextOutput
|
import androidx.media3.exoplayer.text.TextOutput
|
||||||
|
|
||||||
@UnstableApi
|
@OptIn(UnstableApi::class)
|
||||||
class CustomTextRenderer(
|
class CustomTextRenderer(
|
||||||
offset: Long,
|
offset: Long,
|
||||||
output: TextOutput?,
|
output: TextOutput?,
|
||||||
|
|
|
@ -49,7 +49,7 @@ class DownloadFileGenerator(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanDisplayName(name: String): String {
|
private fun cleanDisplayName(name: String): String {
|
||||||
return name.substringBeforeLast('.').trim()
|
return name.substringBeforeLast('.').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,10 @@ import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.lagradost.cloudstream3.CommonActivity
|
import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
|
||||||
import com.lagradost.safefile.SafeFile
|
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink
|
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink
|
||||||
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri
|
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri
|
||||||
|
|
||||||
const val DTAG = "PlayerActivity"
|
|
||||||
|
|
||||||
class DownloadedPlayerActivity : AppCompatActivity() {
|
class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
private val dTAG = "DownloadedPlayerAct"
|
private val dTAG = "DownloadedPlayerAct"
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO
|
||||||
import android.view.animation.AlphaAnimation
|
import android.view.animation.AlphaAnimation
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.graphics.blue
|
import androidx.core.graphics.blue
|
||||||
|
@ -35,6 +36,7 @@ import androidx.core.view.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
||||||
|
@ -50,7 +52,6 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid
|
||||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||||
import com.lagradost.cloudstream3.ui.result.setText
|
import com.lagradost.cloudstream3.ui.result.setText
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
@ -245,6 +246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
fadeAnimation.duration = 100
|
fadeAnimation.duration = 100
|
||||||
fadeAnimation.fillAfter = true
|
fadeAnimation.fillAfter = true
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
val sView = subView
|
val sView = subView
|
||||||
val sStyle = subStyle
|
val sStyle = subStyle
|
||||||
if (sView != null && sStyle != null) {
|
if (sView != null && sStyle != null) {
|
||||||
|
@ -300,42 +302,40 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
|
|
||||||
private fun restoreOrientationWithSensor(activity: Activity) {
|
private fun restoreOrientationWithSensor(activity: Activity) {
|
||||||
val currentOrientation = activity.resources.configuration.orientation
|
val currentOrientation = activity.resources.configuration.orientation
|
||||||
var orientation = 0
|
val orientation = when (currentOrientation) {
|
||||||
when (currentOrientation) {
|
|
||||||
Configuration.ORIENTATION_LANDSCAPE ->
|
Configuration.ORIENTATION_LANDSCAPE ->
|
||||||
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
|
|
||||||
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
|
|
||||||
orientation = dynamicOrientation()
|
|
||||||
|
|
||||||
Configuration.ORIENTATION_PORTRAIT ->
|
Configuration.ORIENTATION_PORTRAIT ->
|
||||||
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
|
||||||
|
else -> dynamicOrientation()
|
||||||
}
|
}
|
||||||
activity.requestedOrientation = orientation
|
activity.requestedOrientation = orientation
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleOrientationWithSensor(activity: Activity) {
|
private fun toggleOrientationWithSensor(activity: Activity) {
|
||||||
val currentOrientation = activity.resources.configuration.orientation
|
val currentOrientation = activity.resources.configuration.orientation
|
||||||
var orientation = 0
|
val orientation: Int = when (currentOrientation) {
|
||||||
when (currentOrientation) {
|
|
||||||
Configuration.ORIENTATION_LANDSCAPE ->
|
Configuration.ORIENTATION_LANDSCAPE ->
|
||||||
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
|
||||||
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
|
|
||||||
orientation = dynamicOrientation()
|
|
||||||
|
|
||||||
Configuration.ORIENTATION_PORTRAIT ->
|
Configuration.ORIENTATION_PORTRAIT ->
|
||||||
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
|
|
||||||
|
else -> dynamicOrientation()
|
||||||
}
|
}
|
||||||
activity.requestedOrientation = orientation
|
activity.requestedOrientation = orientation
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun lockOrientation(activity: Activity) {
|
open fun lockOrientation(activity: Activity) {
|
||||||
val display =
|
@Suppress("DEPRECATION")
|
||||||
|
val display = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
|
||||||
(activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
|
(activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
|
||||||
|
else activity.display!!
|
||||||
val rotation = display.rotation
|
val rotation = display.rotation
|
||||||
val currentOrientation = activity.resources.configuration.orientation
|
val currentOrientation = activity.resources.configuration.orientation
|
||||||
var orientation = 0
|
val orientation: Int
|
||||||
when (currentOrientation) {
|
when (currentOrientation) {
|
||||||
Configuration.ORIENTATION_LANDSCAPE ->
|
Configuration.ORIENTATION_LANDSCAPE ->
|
||||||
orientation =
|
orientation =
|
||||||
|
@ -344,15 +344,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
else
|
else
|
||||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
|
||||||
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
|
|
||||||
orientation = dynamicOrientation()
|
|
||||||
|
|
||||||
Configuration.ORIENTATION_PORTRAIT ->
|
Configuration.ORIENTATION_PORTRAIT ->
|
||||||
orientation =
|
orientation =
|
||||||
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270)
|
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270)
|
||||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
else
|
else
|
||||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
|
|
||||||
|
else -> orientation = dynamicOrientation()
|
||||||
}
|
}
|
||||||
activity.requestedOrientation = orientation
|
activity.requestedOrientation = orientation
|
||||||
}
|
}
|
||||||
|
@ -1167,6 +1166,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("GestureBackNavigation")
|
||||||
private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean {
|
private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean {
|
||||||
if (hasNavigated) {
|
if (hasNavigated) {
|
||||||
autoHide()
|
autoHide()
|
||||||
|
@ -1581,7 +1581,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cs3 is peak media center
|
// cs3 is peak media center
|
||||||
setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV))
|
setRemainingTimeCounter(durationMode || isLayout(TV))
|
||||||
playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ ->
|
playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ ->
|
||||||
updateRemainingTime()
|
updateRemainingTime()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -13,6 +14,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.animation.addListener
|
import androidx.core.animation.addListener
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
|
@ -21,6 +23,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.media3.common.Format.NO_VALUE
|
import androidx.media3.common.Format.NO_VALUE
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
@ -63,6 +66,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.safefile.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import java.io.Serializable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
@ -234,7 +238,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
private fun closestQuality(target: Int?): Qualities {
|
private fun closestQuality(target: Int?): Qualities {
|
||||||
if (target == null) return Qualities.Unknown
|
if (target == null) return Qualities.Unknown
|
||||||
return Qualities.values().minBy { abs(it.value - target) }
|
return Qualities.entries.minBy { abs(it.value - target) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLinkPriority(
|
private fun getLinkPriority(
|
||||||
|
@ -367,8 +371,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||||
binding.subtitleAdapter.adapter = arrayAdapter
|
binding.subtitleAdapter.adapter = arrayAdapter
|
||||||
val adapter =
|
|
||||||
binding.subtitleAdapter.adapter as? ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>
|
|
||||||
|
|
||||||
binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ ->
|
binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ ->
|
||||||
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
|
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
|
||||||
|
@ -379,8 +381,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) {
|
fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) {
|
||||||
currentSubtitles = list
|
currentSubtitles = list
|
||||||
adapter?.clear()
|
arrayAdapter.clear()
|
||||||
adapter?.addAll(currentSubtitles)
|
arrayAdapter.addAll(currentSubtitles)
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentTempMeta = getMetaData()
|
val currentTempMeta = getMetaData()
|
||||||
|
@ -522,7 +524,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
//TODO: Set year text from currently loaded movie on Player
|
//TODO: Set year text from currently loaded movie on Player
|
||||||
//dialog.subtitles_search_year?.setText(currentTempMeta.year)
|
//dialog.subtitles_search_year?.setText(currentTempMeta.year)
|
||||||
}
|
}
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private fun openSubPicker() {
|
private fun openSubPicker() {
|
||||||
try {
|
try {
|
||||||
subsPathPicker.launch(
|
subsPathPicker.launch(
|
||||||
|
@ -795,7 +797,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
settingsManager.edit().putString(
|
settingsManager.edit().putString(
|
||||||
ctx.getString(R.string.subtitles_encoding_key), prefValues[it]
|
ctx.getString(R.string.subtitles_encoding_key), prefValues[it]
|
||||||
).apply()
|
).apply()
|
||||||
|
|
||||||
updateForcedEncoding(ctx)
|
updateForcedEncoding(ctx)
|
||||||
dismiss()
|
dismiss()
|
||||||
player.seekTime(-1) // to update subtitles, a dirty trick
|
player.seekTime(-1) // to update subtitles, a dirty trick
|
||||||
|
@ -1290,7 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
private fun unwrapBundle(savedInstanceState: Bundle?) {
|
private fun unwrapBundle(savedInstanceState: Bundle?) {
|
||||||
Log.i(TAG, "unwrapBundle = $savedInstanceState")
|
Log.i(TAG, "unwrapBundle = $savedInstanceState")
|
||||||
savedInstanceState?.let { bundle ->
|
savedInstanceState?.let { bundle ->
|
||||||
sync.addSyncs(bundle.getSerializable("syncData") as? HashMap<String, String>?)
|
sync.addSyncs(bundle.getSafeSerializable<HashMap<String, String>>("syncData"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1507,3 +1508,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
inline fun <reified T : Serializable> Bundle.getSafeSerializable(key: String) : T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSerializable(key) as? T else getSerializable(key, T::class.java)
|
|
@ -8,7 +8,6 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
|
||||||
enum class PlayerEventType(val value: Int) {
|
enum class PlayerEventType(val value: Int) {
|
||||||
//Stop(-1),
|
|
||||||
Pause(0),
|
Pause(0),
|
||||||
Play(1),
|
Play(1),
|
||||||
SeekForward(2),
|
SeekForward(2),
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.net.Uri
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.amap
|
import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.ui.player.ExtractorUri
|
|
||||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
|
|
@ -29,6 +29,7 @@ import android.os.Message;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.text.Cue;
|
import androidx.media3.common.text.Cue;
|
||||||
|
@ -66,7 +67,7 @@ import java.util.stream.Collectors;
|
||||||
* obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
|
* obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
|
||||||
* is delegated to a {@link TextOutput}.
|
* is delegated to a {@link TextOutput}.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
|
|
||||||
private static final String TAG = "TextRenderer";
|
private static final String TAG = "TextRenderer";
|
||||||
|
@ -74,7 +75,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
/**
|
/**
|
||||||
* @param trackType The track type that the renderer handles. One of the {@link C} {@code
|
* @param trackType The track type that the renderer handles. One of the {@link C} {@code
|
||||||
* TRACK_TYPE_*} constants.
|
* TRACK_TYPE_*} constants.
|
||||||
* @param outputHandler
|
* @param outputHandler todo description
|
||||||
*/
|
*/
|
||||||
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
|
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
|
||||||
super(trackType);
|
super(trackType);
|
||||||
|
@ -416,13 +417,11 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public boolean handleMessage(Message msg) {
|
public boolean handleMessage(Message msg) {
|
||||||
switch (msg.what) {
|
if (msg.what == MSG_UPDATE_OUTPUT) {
|
||||||
case MSG_UPDATE_OUTPUT:
|
invokeUpdateOutputInternal((List<Cue>) msg.obj);
|
||||||
invokeUpdateOutputInternal((List<Cue>) msg.obj);
|
return true;
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
}
|
||||||
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeUpdateOutputInternal(List<Cue> cues) {
|
private void invokeUpdateOutputInternal(List<Cue> cues) {
|
||||||
|
@ -441,7 +440,6 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
}
|
}
|
||||||
).collect(Collectors.toList());
|
).collect(Collectors.toList());
|
||||||
|
|
||||||
output.onCues(fixedCues);
|
|
||||||
output.onCues(new CueGroup(fixedCues, 0L));
|
output.onCues(new CueGroup(fixedCues, 0L));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import android.app.Activity
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.content.ContextCompat.getString
|
import androidx.core.content.ContextCompat.getString
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.ui.player.ExtractorUri
|
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
import com.lagradost.safefile.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PlayerGeneratorViewModel : ViewModel() {
|
class PlayerGeneratorViewModel : ViewModel() {
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "PlayViewGen"
|
const val TAG = "PlayViewGen"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var generator: IGenerator? = null
|
private var generator: IGenerator? = null
|
||||||
|
|
|
@ -4,7 +4,9 @@ import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.ui.SubtitleView
|
import androidx.media3.ui.SubtitleView
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat
|
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat
|
||||||
|
@ -47,6 +49,7 @@ data class SubtitleData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
class PlayerSubtitleHelper {
|
class PlayerSubtitleHelper {
|
||||||
private var activeSubtitles: Set<SubtitleData> = emptySet()
|
private var activeSubtitles: Set<SubtitleData> = emptySet()
|
||||||
private var allSubtitles: Set<SubtitleData> = emptySet()
|
private var allSubtitles: Set<SubtitleData> = emptySet()
|
||||||
|
|
|
@ -239,7 +239,11 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG
|
||||||
// generated images 1:1 to idx of hsl
|
// generated images 1:1 to idx of hsl
|
||||||
private var images: Array<Bitmap?> = arrayOf()
|
private var images: Array<Bitmap?> = arrayOf()
|
||||||
|
|
||||||
private val TAG = "PreviewImgM3u8"
|
companion object {
|
||||||
|
private const val TAG = "PreviewImgM3u8"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// prefixSum[i] = sum(hsl.ts[0..i].time)
|
// prefixSum[i] = sum(hsl.ts[0..i].time)
|
||||||
// where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b
|
// where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b
|
||||||
|
@ -388,13 +392,6 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG
|
||||||
logError(t)
|
logError(t)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
val buffer = hsl.resolveLinkSafe(index) ?: continue
|
|
||||||
tmpFile?.writeBytes(buffer)
|
|
||||||
val buff = FileOutputStream(tmpFile)
|
|
||||||
retriever.setDataSource(buff.fd)
|
|
||||||
val frame = retriever.getFrameAtTime(0L)*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,14 +409,16 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PreviewImgMp4"
|
||||||
|
}
|
||||||
|
|
||||||
override fun hasPreview(): Boolean {
|
override fun hasPreview(): Boolean {
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
return loadedLod >= MIN_LOD
|
return loadedLod >= MIN_LOD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val TAG = "PreviewImgMp4"
|
|
||||||
|
|
||||||
override fun getPreviewImage(fraction: Float): Bitmap? {
|
override fun getPreviewImage(fraction: Float): Bitmap? {
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
if (loadedLod < MIN_LOD) {
|
if (loadedLod < MIN_LOD) {
|
||||||
|
@ -524,7 +523,7 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe
|
||||||
val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
|
val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
|
||||||
Log.i(TAG, "Generating preview for ${fraction * 100}%")
|
Log.i(TAG, "Generating preview for ${fraction * 100}%")
|
||||||
val frame = durationUs * fraction
|
val frame = durationUs * fraction
|
||||||
val img = retriever.image(frame.toLong(), params);
|
val img = retriever.image(frame.toLong(), params)
|
||||||
if (!scope.isActive) return
|
if (!scope.isActive) return
|
||||||
if (img == null || img.width <= 1 || img.height <= 1) continue
|
if (img == null || img.width <= 1 || img.height <= 1) continue
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
|
|
|
@ -17,7 +17,6 @@ class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return PriorityViewHolder(
|
return PriorityViewHolder(
|
||||||
PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false),
|
PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false),
|
||||||
//LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,10 +30,6 @@ class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
|
||||||
val binding: PlayerPrioritizeItemBinding,
|
val binding: PlayerPrioritizeItemBinding,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
fun <T> bind(item: SourcePriority<T>) {
|
fun <T> bind(item: SourcePriority<T>) {
|
||||||
/* val plusButton: ImageView = itemView.add_button
|
|
||||||
val subtractButton: ImageView = itemView.subtract_button
|
|
||||||
val priorityText: TextView = itemView.priority_text
|
|
||||||
val priorityNumber: TextView = itemView.priority_number*/
|
|
||||||
binding.priorityText.text = item.name
|
binding.priorityText.text = item.name
|
||||||
|
|
||||||
fun updatePriority() {
|
fun updatePriority() {
|
||||||
|
|
|
@ -29,8 +29,6 @@ class ProfilesAdapter(
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return ProfilesViewHolder(
|
return ProfilesViewHolder(
|
||||||
PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
|
PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
|
||||||
//LayoutInflater.from(parent.context)
|
|
||||||
// .inflate(R.layout.player_quality_profile_item, parent, false)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.ui.player.source_priority
|
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
|
@ -104,7 +103,7 @@ object QualityDataHelper {
|
||||||
* Must under all circumstances at least return one profile
|
* Must under all circumstances at least return one profile
|
||||||
**/
|
**/
|
||||||
fun getProfiles(): List<QualityProfile> {
|
fun getProfiles(): List<QualityProfile> {
|
||||||
val availableTypes = QualityProfileType.values().toMutableList()
|
val availableTypes = QualityProfileType.entries.toMutableList()
|
||||||
val profiles = (1..PROFILE_COUNT).map { profileNumber ->
|
val profiles = (1..PROFILE_COUNT).map { profileNumber ->
|
||||||
// Get the real type
|
// Get the real type
|
||||||
val type = getQualityProfileType(profileNumber)
|
val type = getQualityProfileType(profileNumber)
|
||||||
|
@ -140,12 +139,12 @@ object QualityDataHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileType.values().forEach {
|
QualityProfileType.entries.forEach {
|
||||||
if (it.unique) insertType(profiles, it)
|
if (it.unique) insertType(profiles, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
debugAssert({
|
debugAssert({
|
||||||
!QualityProfileType.values().all { type ->
|
!QualityProfileType.entries.all { type ->
|
||||||
!type.unique || profiles.any { it.type == type }
|
!type.unique || profiles.any { it.type == type }
|
||||||
}
|
}
|
||||||
}, { "All unique quality types do not exist" })
|
}, { "All unique quality types do not exist" })
|
||||||
|
|
|
@ -65,7 +65,7 @@ class QualityProfileDialog(
|
||||||
|
|
||||||
setDefaultBtt.setOnClickListener {
|
setDefaultBtt.setOnClickListener {
|
||||||
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
|
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
|
||||||
val choices = QualityDataHelper.QualityProfileType.values()
|
val choices = QualityDataHelper.QualityProfileType.entries
|
||||||
.filter { it != QualityDataHelper.QualityProfileType.None }
|
.filter { it != QualityDataHelper.QualityProfileType.None }
|
||||||
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
|
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ class SourcePriorityDialog(
|
||||||
)
|
)
|
||||||
|
|
||||||
qualitiesRecyclerView.adapter = PriorityAdapter(
|
qualitiesRecyclerView.adapter = PriorityAdapter(
|
||||||
Qualities.values().mapNotNull {
|
Qualities.entries.mapNotNull {
|
||||||
SourcePriority(
|
SourcePriority(
|
||||||
it,
|
it,
|
||||||
Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null },
|
Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null },
|
||||||
|
|
|
@ -3,8 +3,6 @@ package com.lagradost.cloudstream3.ui.result
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.annotation.LayoutRes
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -70,8 +68,7 @@ class ActorAdaptor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class CardViewHolder
|
private inner class CardViewHolder(
|
||||||
constructor(
|
|
||||||
val binding: CastItemBinding,
|
val binding: CastItemBinding,
|
||||||
private val focusCallback: (View?) -> Unit = {}
|
private val focusCallback: (View?) -> Unit = {}
|
||||||
) :
|
) :
|
||||||
|
|
|
@ -169,8 +169,7 @@ class EpisodeAdapter(
|
||||||
return cardList.size
|
return cardList.size
|
||||||
}
|
}
|
||||||
|
|
||||||
class EpisodeCardViewHolderLarge
|
class EpisodeCardViewHolderLarge(
|
||||||
constructor(
|
|
||||||
val binding: ResultEpisodeLargeBinding,
|
val binding: ResultEpisodeLargeBinding,
|
||||||
private val hasDownloadSupport: Boolean,
|
private val hasDownloadSupport: Boolean,
|
||||||
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
||||||
|
@ -335,8 +334,7 @@ class EpisodeAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EpisodeCardViewHolderSmall
|
class EpisodeCardViewHolderSmall(
|
||||||
constructor(
|
|
||||||
val binding: ResultEpisodeBinding,
|
val binding: ResultEpisodeBinding,
|
||||||
private val hasDownloadSupport: Boolean,
|
private val hasDownloadSupport: Boolean,
|
||||||
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
||||||
|
|
|
@ -8,18 +8,6 @@ import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
|
||||||
/*
|
|
||||||
class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter<Int>(context, resource) {
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val newConvertView = convertView ?: run {
|
|
||||||
val mInflater = context
|
|
||||||
.getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
|
||||||
mInflater.inflate(resource, null)
|
|
||||||
}
|
|
||||||
getItem(position)?.let { (newConvertView as? ImageView?)?.setImageResource(it) }
|
|
||||||
return newConvertView
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
const val IMAGE_CLICK = 0
|
const val IMAGE_CLICK = 0
|
||||||
const val IMAGE_LONG_CLICK = 1
|
const val IMAGE_LONG_CLICK = 1
|
||||||
|
|
||||||
|
@ -66,8 +54,7 @@ class ImageAdapter(
|
||||||
diffResult.dispatchUpdatesTo(this)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImageViewHolder
|
class ImageViewHolder(val binding: ResultMiniImageBinding) :
|
||||||
constructor(val binding: ResultMiniImageBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(
|
fun bind(
|
||||||
img: Int,
|
img: Int,
|
||||||
|
|
|
@ -78,11 +78,12 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
|
|
||||||
open class ResultFragmentPhone : FullScreenPlayer() {
|
open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener {
|
private val gestureRegionsListener =
|
||||||
override fun onGestureRegionsUpdate(gestureRegions: List<Rect>) {
|
object : PanelsChildGestureRegionObserver.GestureRegionsListener {
|
||||||
binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions)
|
override fun onGestureRegionsUpdate(gestureRegions: List<Rect>) {
|
||||||
|
binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected lateinit var viewModel: ResultViewModel2
|
protected lateinit var viewModel: ResultViewModel2
|
||||||
protected lateinit var syncModel: SyncViewModel
|
protected lateinit var syncModel: SyncViewModel
|
||||||
|
@ -336,7 +337,6 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ===== ===== =====
|
// ===== ===== =====
|
||||||
|
|
||||||
resultBinding?.apply {
|
resultBinding?.apply {
|
||||||
|
@ -430,16 +430,16 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
if (newStatus == null) return@toggleSubscriptionStatus
|
if (newStatus == null) return@toggleSubscriptionStatus
|
||||||
|
|
||||||
val message = if (newStatus) {
|
val message = if (newStatus) {
|
||||||
// Kinda icky to have this here, but it works.
|
// Kinda icky to have this here, but it works.
|
||||||
SubscriptionWorkManager.enqueuePeriodicWork(context)
|
SubscriptionWorkManager.enqueuePeriodicWork(context)
|
||||||
R.string.subscription_new
|
R.string.subscription_new
|
||||||
} else {
|
} else {
|
||||||
R.string.subscription_deleted
|
R.string.subscription_deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||||
showToast(txt(message, name), Toast.LENGTH_SHORT)
|
showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
context?.let { openBatteryOptimizationSettings(it) }
|
context?.let { openBatteryOptimizationSettings(it) }
|
||||||
}
|
}
|
||||||
|
@ -473,8 +473,16 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
if (act.isCastApiAvailable()) {
|
if (act.isCastApiAvailable()) {
|
||||||
try {
|
try {
|
||||||
CastButtonFactory.setUpMediaRouteButton(act, this)
|
CastButtonFactory.setUpMediaRouteButton(act, this)
|
||||||
val castContext = CastContext.getSharedInstance(act.applicationContext)
|
CastContext.getSharedInstance(act.applicationContext) {
|
||||||
isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE
|
it.run()
|
||||||
|
}.addOnCompleteListener {
|
||||||
|
isGone = if (it.isSuccessful) {
|
||||||
|
it.result.castState == CastState.NO_DEVICES_AVAILABLE
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
// this shit leaks for some reason
|
// this shit leaks for some reason
|
||||||
//castContext.addCastStateListener { state ->
|
//castContext.addCastStateListener { state ->
|
||||||
// media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE
|
// media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE
|
||||||
|
@ -961,12 +969,12 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
|
|
||||||
setOnClickListener { fab ->
|
setOnClickListener { fab ->
|
||||||
activity?.showBottomDialog(
|
activity?.showBottomDialog(
|
||||||
WatchType.values().map { fab.context.getString(it.stringRes) }.toList(),
|
WatchType.entries.map { fab.context.getString(it.stringRes) }.toList(),
|
||||||
watchType.ordinal,
|
watchType.ordinal,
|
||||||
fab.context.getString(R.string.action_add_to_bookmarks),
|
fab.context.getString(R.string.action_add_to_bookmarks),
|
||||||
showApply = false,
|
showApply = false,
|
||||||
{}) {
|
{}) {
|
||||||
viewModel.updateWatchStatus(WatchType.values()[it], context)
|
viewModel.updateWatchStatus(WatchType.entries[it], context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1046,7 +1054,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
text?.asStringNull(ctx) ?: return@mapNotNull null
|
text?.asStringNull(ctx) ?: return@mapNotNull null
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
viewModel.changeDubStatus(DubStatus.values()[itemId])
|
viewModel.changeDubStatus(DubStatus.entries[itemId])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1103,7 +1111,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener)
|
PanelsChildGestureRegionObserver.Provider.get()
|
||||||
|
.addGestureRegionsUpdateListener(gestureRegionsListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
|
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
|
||||||
|
|
|
@ -56,7 +56,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
|
||||||
class ResultFragmentTv : Fragment() {
|
class ResultFragmentTv : Fragment() {
|
||||||
protected lateinit var viewModel: ResultViewModel2
|
private lateinit var viewModel: ResultViewModel2
|
||||||
private var binding: FragmentResultTvBinding? = null
|
private var binding: FragmentResultTvBinding? = null
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
@ -418,10 +418,6 @@ class ResultFragmentTv : Fragment() {
|
||||||
|
|
||||||
resultCastItems.layoutManager = object : LinearListLayout(view.context) {
|
resultCastItems.layoutManager = object : LinearListLayout(view.context) {
|
||||||
|
|
||||||
override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
|
|
||||||
return super.onInterceptFocusSearch(focused, direction)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRequestChildFocus(
|
override fun onRequestChildFocus(
|
||||||
parent: RecyclerView,
|
parent: RecyclerView,
|
||||||
state: RecyclerView.State,
|
state: RecyclerView.State,
|
||||||
|
@ -649,7 +645,7 @@ class ResultFragmentTv : Fragment() {
|
||||||
|
|
||||||
binding?.apply {
|
binding?.apply {
|
||||||
|
|
||||||
(data as? Resource.Success)?.value?.let { (text, ep) ->
|
(data as? Resource.Success)?.value?.let { (_, ep) ->
|
||||||
|
|
||||||
resultPlayMovieButton.setOnClickListener {
|
resultPlayMovieButton.setOnClickListener {
|
||||||
viewModel.handleAction(
|
viewModel.handleAction(
|
||||||
|
@ -817,45 +813,8 @@ class ResultFragmentTv : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Okay so what is this fuckery?
|
|
||||||
* Basically Android TV will crash if you request a new focus while
|
|
||||||
* the adapter gets updated.
|
|
||||||
*
|
|
||||||
* This means that if you load thumbnails and request a next focus at the same time
|
|
||||||
* the app will crash without any way to catch it!
|
|
||||||
*
|
|
||||||
* How to bypass this?
|
|
||||||
* This code basically steals the focus for 500ms and puts it in an inescapable view
|
|
||||||
* then lets out the focus by requesting focus to result_episodes
|
|
||||||
*/
|
|
||||||
|
|
||||||
val hasEpisodes =
|
|
||||||
!(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty()
|
|
||||||
/*val focus = activity?.currentFocus
|
|
||||||
|
|
||||||
if (hasEpisodes) {
|
|
||||||
// Make it impossible to focus anywhere else!
|
|
||||||
temporaryNoFocus.isFocusable = true
|
|
||||||
temporaryNoFocus.requestFocus()
|
|
||||||
}*/
|
|
||||||
|
|
||||||
(resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value)
|
(resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value)
|
||||||
|
|
||||||
/* if (hasEpisodes) main {
|
|
||||||
|
|
||||||
delay(500)
|
|
||||||
// This might make some people sad as it changes the focus when leaving an episode :(
|
|
||||||
if(focus?.requestFocus() == true) {
|
|
||||||
temporaryNoFocus.isFocusable = false
|
|
||||||
return@main
|
|
||||||
}
|
|
||||||
temporaryNoFocus.isFocusable = false
|
|
||||||
temporaryNoFocus.requestFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNoFocus())
|
|
||||||
binding?.resultEpisodes?.requestFocus()*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2723,7 +2723,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
val id: Int?,
|
val id: Int?,
|
||||||
) : LoadResponse
|
) : LoadResponse
|
||||||
|
|
||||||
fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe {
|
fun loadSmall(searchResponse: SearchResponse) = ioSafe {
|
||||||
val url = searchResponse.url
|
val url = searchResponse.url
|
||||||
_page.postValue(Resource.Loading(url))
|
_page.postValue(Resource.Loading(url))
|
||||||
_episodes.postValue(Resource.Loading())
|
_episodes.postValue(Resource.Loading())
|
||||||
|
|
|
@ -63,8 +63,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<Recycler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class SelectViewHolder
|
private class SelectViewHolder(
|
||||||
constructor(
|
|
||||||
binding: ResultSelectionBinding,
|
binding: ResultSelectionBinding,
|
||||||
) :
|
) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import com.lagradost.cloudstream3.SearchResponse
|
||||||
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
|
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
|
||||||
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
|
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
|
||||||
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
|
import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class SearchAdapter(
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
|
||||||
val layout =
|
val layout =
|
||||||
if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate(
|
if (parent.context.isBottomLayout()) SearchResultGridExpandedBinding.inflate(
|
||||||
inflater,
|
inflater,
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
|
@ -83,8 +83,7 @@ class SearchAdapter(
|
||||||
diffResult.dispatchUpdatesTo(this)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
class CardViewHolder
|
class CardViewHolder(
|
||||||
constructor(
|
|
||||||
val binding: ViewBinding,
|
val binding: ViewBinding,
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||||
resView: AutofitRecyclerView
|
resView: AutofitRecyclerView
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
package com.lagradost.cloudstream3.ui.search
|
package com.lagradost.cloudstream3.ui.search
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.R
|
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.databinding.AccountSingleBinding
|
|
||||||
import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding
|
import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding
|
||||||
|
|
||||||
data class SearchHistoryItem(
|
data class SearchHistoryItem(
|
||||||
|
@ -63,8 +58,7 @@ class SearchHistoryAdaptor(
|
||||||
diffResult.dispatchUpdatesTo(this)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
class CardViewHolder
|
class CardViewHolder(
|
||||||
constructor(
|
|
||||||
val binding: SearchHistoryItemBinding,
|
val binding: SearchHistoryItemBinding,
|
||||||
private val clickCallback: (SearchHistoryCallback) -> Unit,
|
private val clickCallback: (SearchHistoryCallback) -> Unit,
|
||||||
) :
|
) :
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.ui.search
|
package com.lagradost.cloudstream3.ui.search
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
@ -37,16 +38,12 @@ object SearchResultBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@SuppressLint("StringFormatInvalid")
|
||||||
* @param nextFocusBehavior True if first, False if last, Null if between.
|
|
||||||
* Used to prevent escaping the adapter horizontally (focus wise).
|
|
||||||
*/
|
|
||||||
fun bind(
|
fun bind(
|
||||||
clickCallback: (SearchClickCallback) -> Unit,
|
clickCallback: (SearchClickCallback) -> Unit,
|
||||||
card: SearchResponse,
|
card: SearchResponse,
|
||||||
position: Int,
|
position: Int,
|
||||||
itemView: View,
|
itemView: View,
|
||||||
nextFocusBehavior: Boolean? = null,
|
|
||||||
nextFocusUp: Int? = null,
|
nextFocusUp: Int? = null,
|
||||||
nextFocusDown: Int? = null,
|
nextFocusDown: Int? = null,
|
||||||
colorCallback : ((Palette) -> Unit)? = null
|
colorCallback : ((Palette) -> Unit)? = null
|
||||||
|
|
|
@ -3,11 +3,9 @@ package com.lagradost.cloudstream3.ui.search
|
||||||
import com.lagradost.cloudstream3.SearchQuality
|
import com.lagradost.cloudstream3.SearchQuality
|
||||||
import com.lagradost.cloudstream3.SearchResponse
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
|
||||||
|
|
||||||
|
//TODO Relevance of this class since it's not used
|
||||||
class SyncSearchViewModel {
|
class SyncSearchViewModel {
|
||||||
private val repos = SyncApis
|
|
||||||
|
|
||||||
data class SyncSearchResultSearchResponse(
|
data class SyncSearchResultSearchResponse(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val url: String,
|
override val url: String,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.ui.settings
|
package com.lagradost.cloudstream3.ui.settings
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -13,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo)
|
class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo)
|
||||||
|
|
||||||
class AccountAdapter(
|
class AccountAdapter(
|
||||||
val cardList: List<AuthAPI.LoginInfo>,
|
private val cardList: List<AuthAPI.LoginInfo>,
|
||||||
private val clickCallback: (AccountClickCallback) -> Unit
|
private val clickCallback: (AccountClickCallback) -> Unit
|
||||||
) :
|
) :
|
||||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
@ -42,12 +43,12 @@ class AccountAdapter(
|
||||||
return cardList[position].accountIndex.toLong()
|
return cardList[position].accountIndex.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
class CardViewHolder
|
class CardViewHolder(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) :
|
||||||
constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
// private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
|
// private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
|
||||||
// private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
|
// private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatInvalid")
|
||||||
fun bind(card: AuthAPI.LoginInfo) {
|
fun bind(card: AuthAPI.LoginInfo) {
|
||||||
// just in case name is null account index will show, should never happened
|
// just in case name is null account index will show, should never happened
|
||||||
binding.accountName.text = card.name ?: "%s %d".format(
|
binding.accountName.text = card.name ?: "%s %d".format(
|
||||||
|
|
|
@ -26,7 +26,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper
|
import com.lagradost.cloudstream3.utils.UIHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||||
|
|
|
@ -28,10 +28,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.network.initClient
|
import com.lagradost.cloudstream3.network.initClient
|
||||||
import com.lagradost.cloudstream3.ui.EasterEggMonke
|
import com.lagradost.cloudstream3.ui.EasterEggMonke
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount
|
import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
|
||||||
|
@ -108,7 +107,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
||||||
getPref(R.string.hide_player_control_names_key)?.hideOn(TV)
|
getPref(R.string.hide_player_control_names_key)?.hideOn(TV)
|
||||||
|
|
||||||
getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener {
|
getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener {
|
||||||
val prefValues = Qualities.values().map { it.value }.reversed().toMutableList()
|
val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList()
|
||||||
prefValues.remove(Qualities.Unknown.value)
|
prefValues.remove(Qualities.Unknown.value)
|
||||||
|
|
||||||
val prefNames = prefValues.map { Qualities.getStringByInt(it) }
|
val prefNames = prefValues.map { Qualities.getStringByInt(it) }
|
||||||
|
@ -116,7 +115,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
||||||
val currentQuality =
|
val currentQuality =
|
||||||
settingsManager.getInt(
|
settingsManager.getInt(
|
||||||
getString(R.string.quality_pref_key),
|
getString(R.string.quality_pref_key),
|
||||||
Qualities.values().last().value
|
Qualities.entries.last().value
|
||||||
)
|
)
|
||||||
|
|
||||||
activity?.showBottomDialog(
|
activity?.showBottomDialog(
|
||||||
|
@ -132,7 +131,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener {
|
getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener {
|
||||||
val prefValues = Qualities.values().map { it.value }.reversed().toMutableList()
|
val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList()
|
||||||
prefValues.remove(Qualities.Unknown.value)
|
prefValues.remove(Qualities.Unknown.value)
|
||||||
|
|
||||||
val prefNames = prefValues.map { Qualities.getStringByInt(it) }
|
val prefNames = prefValues.map { Qualities.getStringByInt(it) }
|
||||||
|
@ -140,7 +139,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
||||||
val currentQuality =
|
val currentQuality =
|
||||||
settingsManager.getInt(
|
settingsManager.getInt(
|
||||||
getString(R.string.quality_pref_mobile_data_key),
|
getString(R.string.quality_pref_mobile_data_key),
|
||||||
Qualities.values().last().value
|
Qualities.entries.last().value
|
||||||
)
|
)
|
||||||
|
|
||||||
activity?.showBottomDialog(
|
activity?.showBottomDialog(
|
||||||
|
|
|
@ -34,7 +34,7 @@ class SettingsProviders : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
getPref(R.string.display_sub_key)?.setOnPreferenceClickListener {
|
getPref(R.string.display_sub_key)?.setOnPreferenceClickListener {
|
||||||
activity?.getApiDubstatusSettings()?.let { current ->
|
activity?.getApiDubstatusSettings()?.let { current ->
|
||||||
val dublist = DubStatus.values()
|
val dublist = DubStatus.entries
|
||||||
val names = dublist.map { it.name }
|
val names = dublist.map { it.name }
|
||||||
|
|
||||||
val currentList = ArrayList<Int>()
|
val currentList = ArrayList<Int>()
|
||||||
|
|
|
@ -128,7 +128,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.saveBtt.setOnClickListener {
|
binding.saveBtt.setOnClickListener {
|
||||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.getDefault()).format(Date(currentTimeMillis()))
|
||||||
var fileStream: OutputStream? = null
|
var fileStream: OutputStream? = null
|
||||||
try {
|
try {
|
||||||
fileStream = VideoDownloadManager.setupStream(
|
fileStream = VideoDownloadManager.setupStream(
|
||||||
|
@ -169,10 +169,10 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
||||||
prefValues.indexOf(currentInstaller),
|
prefValues.indexOf(currentInstaller),
|
||||||
getString(R.string.apk_installer_settings),
|
getString(R.string.apk_installer_settings),
|
||||||
true,
|
true,
|
||||||
{}) {
|
{}) { num ->
|
||||||
try {
|
try {
|
||||||
settingsManager.edit()
|
settingsManager.edit()
|
||||||
.putInt(getString(R.string.apk_installer_key), prefValues[it])
|
.putInt(getString(R.string.apk_installer_key), prefValues[num])
|
||||||
.apply()
|
.apply()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -209,9 +209,9 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
||||||
prefValues.indexOf(current),
|
prefValues.indexOf(current),
|
||||||
getString(R.string.automatic_plugin_download_mode_title),
|
getString(R.string.automatic_plugin_download_mode_title),
|
||||||
true,
|
true,
|
||||||
{}) {
|
{}) { num ->
|
||||||
settingsManager.edit()
|
settingsManager.edit()
|
||||||
.putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply()
|
.putInt(getString(R.string.auto_download_plugins_key), prefValues[num]).apply()
|
||||||
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
|
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
|
||||||
}
|
}
|
||||||
return@setOnPreferenceClickListener true
|
return@setOnPreferenceClickListener true
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package com.lagradost.cloudstream3.ui.settings.extensions
|
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.text.format.Formatter.formatShortFileSize
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
@ -27,11 +29,10 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
|
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import org.junit.Assert
|
|
||||||
import org.junit.Test
|
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
import kotlin.math.log10
|
import kotlin.math.log10
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
|
||||||
data class PluginViewData(
|
data class PluginViewData(
|
||||||
|
@ -95,21 +96,13 @@ class PluginAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int {
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int {
|
||||||
if (current >= max) return max
|
if (current >= max) return max
|
||||||
if (current >= target) return current
|
if (current >= target) return current
|
||||||
return findClosestBase2(target, current * 2, max)
|
return findClosestBase2(target, current * 2, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testFindClosestBase2() {
|
|
||||||
Assert.assertEquals(16, findClosestBase2(0))
|
|
||||||
Assert.assertEquals(256, findClosestBase2(170))
|
|
||||||
Assert.assertEquals(256, findClosestBase2(256))
|
|
||||||
Assert.assertEquals(512, findClosestBase2(257))
|
|
||||||
Assert.assertEquals(512, findClosestBase2(700))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val iconSizeExact = 32.toPx
|
private val iconSizeExact = 32.toPx
|
||||||
private val iconSize by lazy {
|
private val iconSize by lazy {
|
||||||
findClosestBase2(iconSizeExact, 16, 512)
|
findClosestBase2(iconSizeExact, 16, 512)
|
||||||
|
@ -122,10 +115,7 @@ class PluginAdapter(
|
||||||
val base = value / 3
|
val base = value / 3
|
||||||
return if (value >= 3 && base < suffix.size) {
|
return if (value >= 3 && base < suffix.size) {
|
||||||
DecimalFormat("#0.00").format(
|
DecimalFormat("#0.00").format(
|
||||||
numValue / Math.pow(
|
numValue / 10.0.pow((base * 3).toDouble())
|
||||||
10.0,
|
|
||||||
(base * 3).toDouble()
|
|
||||||
)
|
|
||||||
) + suffix[base]
|
) + suffix[base]
|
||||||
} else {
|
} else {
|
||||||
DecimalFormat().format(numValue)
|
DecimalFormat().format(numValue)
|
||||||
|
@ -136,6 +126,7 @@ class PluginAdapter(
|
||||||
inner class PluginViewHolder(val binding: RepositoryItemBinding) :
|
inner class PluginViewHolder(val binding: RepositoryItemBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
fun bind(
|
fun bind(
|
||||||
data: PluginViewData,
|
data: PluginViewData,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -190,7 +190,7 @@ class PluginsFragment : Fragment() {
|
||||||
bindChips(
|
bindChips(
|
||||||
binding?.tvtypesChipsScroll?.tvtypesChips,
|
binding?.tvtypesChipsScroll?.tvtypesChips,
|
||||||
emptyList(),
|
emptyList(),
|
||||||
TvType.values().toList(),
|
TvType.entries.toList(),
|
||||||
callback = { list ->
|
callback = { list ->
|
||||||
pluginViewModel.tvTypes.clear()
|
pluginViewModel.tvTypes.clear()
|
||||||
pluginViewModel.tvTypes.addAll(list.map { it.name })
|
pluginViewModel.tvTypes.addAll(list.map { it.name })
|
||||||
|
|
|
@ -10,7 +10,6 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
|
|
||||||
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
|
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
|
|
@ -10,7 +10,6 @@ import androidx.core.util.forEach
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding
|
import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding
|
||||||
|
|
|
@ -15,8 +15,11 @@ import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.media3.common.text.Cue
|
import androidx.media3.common.text.Cue
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.google.android.gms.cast.TextTrackStyle
|
import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED
|
||||||
import com.google.android.gms.cast.TextTrackStyle.*
|
import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DROP_SHADOW
|
||||||
|
import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_NONE
|
||||||
|
import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE
|
||||||
|
import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_RAISED
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
||||||
|
@ -42,7 +45,7 @@ data class SaveChromeCaptionStyle(
|
||||||
@JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null,
|
@JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null,
|
||||||
@JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent
|
@JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent
|
||||||
@JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK
|
@JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK
|
||||||
@JsonProperty("edgeType") var edgeType: Int = TextTrackStyle.EDGE_TYPE_OUTLINE,
|
@JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE,
|
||||||
@JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE,
|
@JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE,
|
||||||
@JsonProperty("fontScale") var fontScale: Float = 1.05f,
|
@JsonProperty("fontScale") var fontScale: Float = 1.05f,
|
||||||
@JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT,
|
@JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT,
|
||||||
|
@ -99,7 +102,7 @@ class ChromecastSubtitlesFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onColorSelected(stuff: Pair<Int, Int>) {
|
private fun onColorSelected(stuff: Pair<Int, Int>) {
|
||||||
context?.setColor(stuff.first, stuff.second)
|
setColor(stuff.first, stuff.second)
|
||||||
if (hide)
|
if (hide)
|
||||||
activity?.hideSystemUI()
|
activity?.hideSystemUI()
|
||||||
}
|
}
|
||||||
|
@ -122,7 +125,7 @@ class ChromecastSubtitlesFragment : Fragment() {
|
||||||
return if (color == Color.TRANSPARENT) Color.BLACK else color
|
return if (color == Color.TRANSPARENT) Color.BLACK else color
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.setColor(id: Int, color: Int?) {
|
private fun setColor(id: Int, color: Int?) {
|
||||||
val realColor = color ?: getDefColor(id)
|
val realColor = color ?: getDefColor(id)
|
||||||
when (id) {
|
when (id) {
|
||||||
0 -> state.foregroundColor = realColor
|
0 -> state.foregroundColor = realColor
|
||||||
|
@ -135,7 +138,7 @@ class ChromecastSubtitlesFragment : Fragment() {
|
||||||
updateState()
|
updateState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.updateState() {
|
private fun updateState() {
|
||||||
//subtitle_text?.setStyle(fromSaveToStyle(state))
|
//subtitle_text?.setStyle(fromSaveToStyle(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +176,7 @@ class ChromecastSubtitlesFragment : Fragment() {
|
||||||
fixPaddingStatusbar(binding?.subsRoot)
|
fixPaddingStatusbar(binding?.subsRoot)
|
||||||
|
|
||||||
state = getCurrentSavedStyle()
|
state = getCurrentSavedStyle()
|
||||||
context?.updateState()
|
updateState()
|
||||||
|
|
||||||
val isTvSettings = isLayout(TV or EMULATOR)
|
val isTvSettings = isLayout(TV or EMULATOR)
|
||||||
|
|
||||||
|
@ -195,7 +198,7 @@ class ChromecastSubtitlesFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setOnLongClickListener {
|
this.setOnLongClickListener {
|
||||||
it.context.setColor(id, null)
|
setColor(id, null)
|
||||||
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||||
return@setOnLongClickListener true
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
|
@ -247,13 +250,13 @@ class ChromecastSubtitlesFragment : Fragment() {
|
||||||
dismissCallback
|
dismissCallback
|
||||||
) { index ->
|
) { index ->
|
||||||
state.edgeType = edgeTypes.map { it.first }[index]
|
state.edgeType = edgeTypes.map { it.first }[index]
|
||||||
textView.context.updateState()
|
updateState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding?.subsEdgeType?.setOnLongClickListener {
|
binding?.subsEdgeType?.setOnLongClickListener {
|
||||||
state.edgeType = defaultState.edgeType
|
state.edgeType = defaultState.edgeType
|
||||||
it.context.updateState()
|
updateState()
|
||||||
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||||
return@setOnLongClickListener true
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
|
@ -323,12 +326,12 @@ class ChromecastSubtitlesFragment : Fragment() {
|
||||||
dismissCallback
|
dismissCallback
|
||||||
) { index ->
|
) { index ->
|
||||||
state.fontFamily = fontTypes.map { it.first }[index]
|
state.fontFamily = fontTypes.map { it.first }[index]
|
||||||
textView.context.updateState()
|
updateState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding?.subsFont?.setOnLongClickListener { textView ->
|
binding?.subsFont?.setOnLongClickListener { _ ->
|
||||||
state.fontFamily = defaultState.fontFamily
|
state.fontFamily = defaultState.fontFamily
|
||||||
textView.context.updateState()
|
updateState()
|
||||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||||
return@setOnLongClickListener true
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,13 @@ import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.FontRes
|
import androidx.annotation.FontRes
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import androidx.media3.common.text.Cue
|
import androidx.media3.common.text.Cue
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.ui.CaptionStyleCompat
|
import androidx.media3.ui.CaptionStyleCompat
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
@ -28,7 +30,6 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding
|
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
|
@ -46,7 +47,7 @@ const val SUBTITLE_KEY = "subtitle_settings"
|
||||||
const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select"
|
const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select"
|
||||||
const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download"
|
const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download"
|
||||||
|
|
||||||
data class SaveCaptionStyle(
|
data class SaveCaptionStyle @OptIn(UnstableApi::class) constructor(
|
||||||
@JsonProperty("foregroundColor") var foregroundColor: Int,
|
@JsonProperty("foregroundColor") var foregroundColor: Int,
|
||||||
@JsonProperty("backgroundColor") var backgroundColor: Int,
|
@JsonProperty("backgroundColor") var backgroundColor: Int,
|
||||||
@JsonProperty("windowColor") var windowColor: Int,
|
@JsonProperty("windowColor") var windowColor: Int,
|
||||||
|
@ -67,7 +68,7 @@ data class SaveCaptionStyle(
|
||||||
|
|
||||||
const val DEF_SUBS_ELEVATION = 20
|
const val DEF_SUBS_ELEVATION = 20
|
||||||
|
|
||||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
@OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
class SubtitlesFragment : Fragment() {
|
class SubtitlesFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
val applyStyleEvent = Event<SaveCaptionStyle>()
|
val applyStyleEvent = Event<SaveCaptionStyle>()
|
||||||
|
@ -167,7 +168,7 @@ class SubtitlesFragment : Fragment() {
|
||||||
activity?.hideSystemUI()
|
activity?.hideSystemUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDialogDismissed(id: Int) {
|
private fun onDialogDismissed(@Suppress("UNUSED_PARAMETER") id: Int) {
|
||||||
if (hide)
|
if (hide)
|
||||||
activity?.hideSystemUI()
|
activity?.hideSystemUI()
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ object EpisodeSkip {
|
||||||
startMs = start,
|
startMs = start,
|
||||||
endMs = end
|
endMs = end
|
||||||
)
|
)
|
||||||
}?.let { list ->
|
}.let { list ->
|
||||||
out.addAll(list)
|
out.addAll(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,6 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.tvprovider.media.tv.*
|
import androidx.tvprovider.media.tv.*
|
||||||
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
|
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
import com.google.android.gms.cast.framework.CastState
|
import com.google.android.gms.cast.framework.CastState
|
||||||
import com.google.android.gms.common.ConnectionResult
|
import com.google.android.gms.common.ConnectionResult
|
||||||
|
@ -58,7 +57,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEv
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
|
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
|
||||||
import com.lagradost.cloudstream3.ui.WebviewFragment
|
import com.lagradost.cloudstream3.ui.WebviewFragment
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
|
@ -161,7 +160,7 @@ object AppContextUtils {
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setPosterArtUri(Uri.parse(card.posterUrl))
|
.setPosterArtUri(Uri.parse(card.posterUrl))
|
||||||
.setIntentUri(Uri.parse(card.id?.let {
|
.setIntentUri(Uri.parse(card.id?.let {
|
||||||
"$appStringResumeWatching://$it"
|
"$APP_STRING_RESUME_WATCHING://$it"
|
||||||
} ?: card.url))
|
} ?: card.url))
|
||||||
.setInternalProviderId(card.url)
|
.setInternalProviderId(card.url)
|
||||||
.setLastEngagementTimeUtcMillis(
|
.setLastEngagementTimeUtcMillis(
|
||||||
|
|
|
@ -81,12 +81,12 @@ object BackupUtils {
|
||||||
|
|
||||||
// Kinda hack, but I couldn't think of a better way
|
// Kinda hack, but I couldn't think of a better way
|
||||||
data class BackupVars(
|
data class BackupVars(
|
||||||
@JsonProperty("_Bool") val _Bool: Map<String, Boolean>?,
|
@JsonProperty("_Bool") val bool: Map<String, Boolean>?,
|
||||||
@JsonProperty("_Int") val _Int: Map<String, Int>?,
|
@JsonProperty("_Int") val int: Map<String, Int>?,
|
||||||
@JsonProperty("_String") val _String: Map<String, String>?,
|
@JsonProperty("_String") val string: Map<String, String>?,
|
||||||
@JsonProperty("_Float") val _Float: Map<String, Float>?,
|
@JsonProperty("_Float") val float: Map<String, Float>?,
|
||||||
@JsonProperty("_Long") val _Long: Map<String, Long>?,
|
@JsonProperty("_Long") val long: Map<String, Long>?,
|
||||||
@JsonProperty("_StringSet") val _StringSet: Map<String, Set<String>?>?,
|
@JsonProperty("_StringSet") val stringSet: Map<String, Set<String>?>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BackupFile(
|
data class BackupFile(
|
||||||
|
@ -134,21 +134,21 @@ object BackupUtils {
|
||||||
) {
|
) {
|
||||||
if (context == null) return
|
if (context == null) return
|
||||||
if (restoreSettings) {
|
if (restoreSettings) {
|
||||||
context.restoreMap(backupFile.settings._Bool, true)
|
context.restoreMap(backupFile.settings.bool, true)
|
||||||
context.restoreMap(backupFile.settings._Int, true)
|
context.restoreMap(backupFile.settings.int, true)
|
||||||
context.restoreMap(backupFile.settings._String, true)
|
context.restoreMap(backupFile.settings.string, true)
|
||||||
context.restoreMap(backupFile.settings._Float, true)
|
context.restoreMap(backupFile.settings.float, true)
|
||||||
context.restoreMap(backupFile.settings._Long, true)
|
context.restoreMap(backupFile.settings.long, true)
|
||||||
context.restoreMap(backupFile.settings._StringSet, true)
|
context.restoreMap(backupFile.settings.stringSet, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restoreDataStore) {
|
if (restoreDataStore) {
|
||||||
context.restoreMap(backupFile.datastore._Bool)
|
context.restoreMap(backupFile.datastore.bool)
|
||||||
context.restoreMap(backupFile.datastore._Int)
|
context.restoreMap(backupFile.datastore.int)
|
||||||
context.restoreMap(backupFile.datastore._String)
|
context.restoreMap(backupFile.datastore.string)
|
||||||
context.restoreMap(backupFile.datastore._Float)
|
context.restoreMap(backupFile.datastore.float)
|
||||||
context.restoreMap(backupFile.datastore._Long)
|
context.restoreMap(backupFile.datastore.long)
|
||||||
context.restoreMap(backupFile.datastore._StringSet)
|
context.restoreMap(backupFile.datastore.stringSet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,16 +56,27 @@ data class Editor(
|
||||||
) {
|
) {
|
||||||
/** Always remember to call apply after */
|
/** Always remember to call apply after */
|
||||||
fun<T> setKeyRaw(path: String, value: T) {
|
fun<T> setKeyRaw(path: String, value: T) {
|
||||||
when (value) {
|
@Suppress("UNCHECKED_CAST")
|
||||||
is Boolean -> editor.putBoolean(path, value)
|
if (isStringSet(value)) {
|
||||||
is Int -> editor.putInt(path, value)
|
editor.putStringSet(path, value as Set<String>)
|
||||||
is String -> editor.putString(path, value)
|
} else {
|
||||||
is Float -> editor.putFloat(path, value)
|
when (value) {
|
||||||
is Long -> editor.putLong(path, value)
|
is Boolean -> editor.putBoolean(path, value)
|
||||||
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
|
is Int -> editor.putInt(path, value)
|
||||||
|
is String -> editor.putString(path, value)
|
||||||
|
is Float -> editor.putFloat(path, value)
|
||||||
|
is Long -> editor.putLong(path, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isStringSet(value: Any?) : Boolean {
|
||||||
|
if (value is Set<*>) {
|
||||||
|
return value.filterIsInstance<String>().size == value.size
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fun apply() {
|
fun apply() {
|
||||||
editor.apply()
|
editor.apply()
|
||||||
System.gc()
|
System.gc()
|
||||||
|
|
|
@ -7,7 +7,6 @@ import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO
|
||||||
|
|
|
@ -32,26 +32,26 @@ import java.io.InputStreamReader
|
||||||
|
|
||||||
class InAppUpdater {
|
class InAppUpdater {
|
||||||
companion object {
|
companion object {
|
||||||
const val GITHUB_USER_NAME = "recloudstream"
|
private const val GITHUB_USER_NAME = "recloudstream"
|
||||||
const val GITHUB_REPO = "cloudstream"
|
private const val GITHUB_REPO = "cloudstream"
|
||||||
|
|
||||||
const val LOG_TAG = "InAppUpdater"
|
private const val LOG_TAG = "InAppUpdater"
|
||||||
|
|
||||||
// === IN APP UPDATER ===
|
// === IN APP UPDATER ===
|
||||||
data class GithubAsset(
|
data class GithubAsset(
|
||||||
@JsonProperty("name") val name: String,
|
@JsonProperty("name") val name: String,
|
||||||
@JsonProperty("size") val size: Int, // Size bytes
|
@JsonProperty("size") val size: Int, // Size bytes
|
||||||
@JsonProperty("browser_download_url") val browser_download_url: String, // download link
|
@JsonProperty("browser_download_url") val browserDownloadUrl: String, // download link
|
||||||
@JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive
|
@JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GithubRelease(
|
data class GithubRelease(
|
||||||
@JsonProperty("tag_name") val tag_name: String, // Version code
|
@JsonProperty("tag_name") val tagName: String, // Version code
|
||||||
@JsonProperty("body") val body: String, // Desc
|
@JsonProperty("body") val body: String, // Desc
|
||||||
@JsonProperty("assets") val assets: List<GithubAsset>,
|
@JsonProperty("assets") val assets: List<GithubAsset>,
|
||||||
@JsonProperty("target_commitish") val target_commitish: String, // branch
|
@JsonProperty("target_commitish") val targetCommitish: String, // branch
|
||||||
@JsonProperty("prerelease") val prerelease: Boolean,
|
@JsonProperty("prerelease") val prerelease: Boolean,
|
||||||
@JsonProperty("node_id") val node_id: String //Node Id
|
@JsonProperty("node_id") val nodeId: String //Node Id
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GithubObject(
|
data class GithubObject(
|
||||||
|
@ -61,7 +61,7 @@ class InAppUpdater {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GithubTag(
|
data class GithubTag(
|
||||||
@JsonProperty("object") val github_object: GithubObject,
|
@JsonProperty("object") val githubObject: GithubObject,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Update(
|
data class Update(
|
||||||
|
@ -114,7 +114,7 @@ class InAppUpdater {
|
||||||
response.filter { rel ->
|
response.filter { rel ->
|
||||||
!rel.prerelease
|
!rel.prerelease
|
||||||
}.sortedWith(compareBy { release ->
|
}.sortedWith(compareBy { release ->
|
||||||
release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 ->
|
release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 ->
|
||||||
versionRegex.find(
|
versionRegex.find(
|
||||||
it1
|
it1
|
||||||
)?.groupValues?.let {
|
)?.groupValues?.let {
|
||||||
|
@ -134,7 +134,7 @@ class InAppUpdater {
|
||||||
foundAsset?.name?.let { assetName ->
|
foundAsset?.name?.let { assetName ->
|
||||||
val foundVersion = versionRegex.find(assetName)
|
val foundVersion = versionRegex.find(assetName)
|
||||||
val shouldUpdate =
|
val shouldUpdate =
|
||||||
if (foundAsset.browser_download_url != "" && foundVersion != null) currentVersion?.versionName?.let { versionName ->
|
if (foundAsset.browserDownloadUrl != "" && foundVersion != null) currentVersion?.versionName?.let { versionName ->
|
||||||
versionRegexLocal.find(versionName)?.groupValues?.let {
|
versionRegexLocal.find(versionName)?.groupValues?.let {
|
||||||
it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt()
|
it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt()
|
||||||
}
|
}
|
||||||
|
@ -146,10 +146,10 @@ class InAppUpdater {
|
||||||
return if (foundVersion != null) {
|
return if (foundVersion != null) {
|
||||||
Update(
|
Update(
|
||||||
shouldUpdate,
|
shouldUpdate,
|
||||||
foundAsset.browser_download_url,
|
foundAsset.browserDownloadUrl,
|
||||||
foundVersion.groupValues[2],
|
foundVersion.groupValues[2],
|
||||||
found.body,
|
found.body,
|
||||||
found.node_id
|
found.nodeId
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Update(false, null, null, null, null)
|
Update(false, null, null, null, null)
|
||||||
|
@ -168,33 +168,33 @@ class InAppUpdater {
|
||||||
|
|
||||||
val found =
|
val found =
|
||||||
response.lastOrNull { rel ->
|
response.lastOrNull { rel ->
|
||||||
rel.prerelease || rel.tag_name == "pre-release"
|
rel.prerelease || rel.tagName == "pre-release"
|
||||||
}
|
}
|
||||||
val foundAsset = found?.assets?.filter { it ->
|
val foundAsset = found?.assets?.filter { it ->
|
||||||
it.content_type == "application/vnd.android.package-archive"
|
it.contentType == "application/vnd.android.package-archive"
|
||||||
}?.getOrNull(0)
|
}?.getOrNull(0)
|
||||||
|
|
||||||
val tagResponse =
|
val tagResponse =
|
||||||
parseJson<GithubTag>(app.get(tagUrl, headers = headers).text)
|
parseJson<GithubTag>(app.get(tagUrl, headers = headers).text)
|
||||||
|
|
||||||
Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.github_object.sha.take(7)}")
|
Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.githubObject.sha.take(7)}")
|
||||||
|
|
||||||
val shouldUpdate =
|
val shouldUpdate =
|
||||||
(getString(R.string.commit_hash)
|
(getString(R.string.commit_hash)
|
||||||
.trim { c -> c.isWhitespace() }
|
.trim { c -> c.isWhitespace() }
|
||||||
.take(7)
|
.take(7)
|
||||||
!=
|
!=
|
||||||
tagResponse.github_object.sha
|
tagResponse.githubObject.sha
|
||||||
.trim { c -> c.isWhitespace() }
|
.trim { c -> c.isWhitespace() }
|
||||||
.take(7))
|
.take(7))
|
||||||
|
|
||||||
return if (foundAsset != null) {
|
return if (foundAsset != null) {
|
||||||
Update(
|
Update(
|
||||||
shouldUpdate,
|
shouldUpdate,
|
||||||
foundAsset.browser_download_url,
|
foundAsset.browserDownloadUrl,
|
||||||
tagResponse.github_object.sha.take(10),
|
tagResponse.githubObject.sha.take(10),
|
||||||
found.body,
|
found.body,
|
||||||
found.node_id
|
found.nodeId
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Update(false, null, null, null, null)
|
Update(false, null, null, null, null)
|
||||||
|
|
|
@ -11,7 +11,6 @@ import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@ -57,7 +56,7 @@ class ApkInstaller(private val service: PackageInstallerService) {
|
||||||
PackageInstaller.STATUS_FAILURE
|
PackageInstaller.STATUS_FAILURE
|
||||||
)) {
|
)) {
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
val userAction = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
val userAction = intent.getSafeParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||||
userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
context.startActivity(userAction)
|
context.startActivity(userAction)
|
||||||
}
|
}
|
||||||
|
@ -146,3 +145,5 @@ class ApkInstaller(private val service: PackageInstallerService) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
inline fun <reified T> Intent.getSafeParcelableExtra(key: String): T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelableExtra(key) else getParcelableExtra(key, T::class.java)
|
||||||
|
|
|
@ -17,8 +17,8 @@ import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
|
||||||
const val packageName = BuildConfig.APPLICATION_ID
|
private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID
|
||||||
const val TAG = "PowerManagerAPI"
|
private const val TAG = "PowerManagerAPI"
|
||||||
|
|
||||||
object BatteryOptimizationChecker {
|
object BatteryOptimizationChecker {
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ object BatteryOptimizationChecker {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
try {
|
try {
|
||||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
.setData(Uri.fromParts("package", packageName, null))
|
.setData(Uri.fromParts("package", PACKAGE_NAME, null))
|
||||||
context.startActivity(intent, Bundle())
|
context.startActivity(intent, Bundle())
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(TAG, "Unable to invoke any intent", t)
|
Log.e(TAG, "Unable to invoke any intent", t)
|
||||||
|
|
|
@ -73,8 +73,8 @@ object SyncUtil {
|
||||||
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text
|
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text
|
||||||
val mapped = parseJson<MalSyncPage?>(response)
|
val mapped = parseJson<MalSyncPage?>(response)
|
||||||
|
|
||||||
val overrideMal = mapped?.malId ?: mapped?.Mal?.id ?: mapped?.Anilist?.malId
|
val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId
|
||||||
val overrideAnilist = mapped?.aniId ?: mapped?.Anilist?.id
|
val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id
|
||||||
|
|
||||||
if (overrideMal != null) {
|
if (overrideMal != null) {
|
||||||
return overrideMal.toString() to overrideAnilist?.toString()
|
return overrideMal.toString() to overrideAnilist?.toString()
|
||||||
|
@ -135,8 +135,8 @@ object SyncUtil {
|
||||||
@JsonProperty("createdAt") val createdAt: String?,
|
@JsonProperty("createdAt") val createdAt: String?,
|
||||||
@JsonProperty("updatedAt") val updatedAt: String?,
|
@JsonProperty("updatedAt") val updatedAt: String?,
|
||||||
@JsonProperty("deletedAt") val deletedAt: String?,
|
@JsonProperty("deletedAt") val deletedAt: String?,
|
||||||
@JsonProperty("Mal") val Mal: Mal?,
|
@JsonProperty("Mal") val mal: Mal?,
|
||||||
@JsonProperty("Anilist") val Anilist: Anilist?,
|
@JsonProperty("Anilist") val anilist: Anilist?,
|
||||||
@JsonProperty("malUrl") val malUrl: String?
|
@JsonProperty("malUrl") val malUrl: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -553,7 +553,7 @@ object UIHelper {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context?.IsBottomLayout(): Boolean {
|
fun Context?.isBottomLayout(): Boolean {
|
||||||
if (this == null) return true
|
if (this == null) return true
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
return settingsManager.getBoolean(getString(R.string.bottom_title_key), true)
|
return settingsManager.getBoolean(getString(R.string.bottom_title_key), true)
|
||||||
|
|
|
@ -293,6 +293,7 @@ object VideoDownloadManager {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
} else {
|
} else {
|
||||||
|
//fixme Specify a better flag
|
||||||
PendingIntent.getActivity(context, 0, intent, 0)
|
PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
}
|
}
|
||||||
builder.setContentIntent(pendingIntent)
|
builder.setContentIntent(pendingIntent)
|
||||||
|
@ -475,10 +476,10 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val reservedChars = "|\\?*<\":>+[]/\'"
|
private const val RESERVED_CHARS = "|\\?*<\":>+[]/\'"
|
||||||
fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String {
|
fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String {
|
||||||
var tempName = name
|
var tempName = name
|
||||||
for (c in reservedChars) {
|
for (c in RESERVED_CHARS) {
|
||||||
tempName = tempName.replace(c, ' ')
|
tempName = tempName.replace(c, ' ')
|
||||||
}
|
}
|
||||||
if (removeSpaces) tempName = tempName.replace(" ", "")
|
if (removeSpaces) tempName = tempName.replace(" ", "")
|
||||||
|
@ -1699,7 +1700,7 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? =
|
fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? =
|
||||||
getDownloadFileInfo(context, id, removeKeys = true)
|
getDownloadFileInfo(context, id)
|
||||||
|
|
||||||
private fun DownloadedFileInfo.toFile(context: Context): SafeFile? {
|
private fun DownloadedFileInfo.toFile(context: Context): SafeFile? {
|
||||||
return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath)
|
return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath)
|
||||||
|
@ -1709,7 +1710,6 @@ object VideoDownloadManager {
|
||||||
private fun getDownloadFileInfo(
|
private fun getDownloadFileInfo(
|
||||||
context: Context,
|
context: Context,
|
||||||
id: Int,
|
id: Int,
|
||||||
removeKeys: Boolean = false
|
|
||||||
): DownloadedFileInfoResult? {
|
): DownloadedFileInfoResult? {
|
||||||
try {
|
try {
|
||||||
val info =
|
val info =
|
||||||
|
|
|
@ -19,7 +19,7 @@ class FlowLayout : ViewGroup {
|
||||||
@SuppressLint("CustomViewStyleable")
|
@SuppressLint("CustomViewStyleable")
|
||||||
internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
|
internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
|
||||||
val t = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout)
|
val t = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout)
|
||||||
itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0);
|
itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0)
|
||||||
t.recycle()
|
t.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PluginAdapterTest {
|
||||||
|
@Test
|
||||||
|
fun testFindClosestBase2() {
|
||||||
|
Assert.assertEquals(16, findClosestBase2(0))
|
||||||
|
Assert.assertEquals(256, findClosestBase2(170))
|
||||||
|
Assert.assertEquals(256, findClosestBase2(256))
|
||||||
|
Assert.assertEquals(512, findClosestBase2(257))
|
||||||
|
Assert.assertEquals(512, findClosestBase2(700))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue