Merge branch 'master' into resource-api
|
@ -206,10 +206,28 @@ task androidSourcesJar(type: Jar) {
|
|||
from android.sourceSets.main.java.srcDirs//full sources
|
||||
}
|
||||
|
||||
// this is used by the gradlew plugin
|
||||
task makeJar(type: Copy) {
|
||||
// after modifying here, you can export. Jar
|
||||
from('build/intermediates/compile_app_classes_jar/debug')
|
||||
into('build') // output location
|
||||
include('classes.jar') // the classes file of the imported rack package
|
||||
dependsOn build
|
||||
into('build')
|
||||
include('classes.jar')
|
||||
dependsOn('build')
|
||||
}
|
||||
|
||||
dokkaHtml {
|
||||
moduleName.set("Cloudstream")
|
||||
dokkaSourceSets {
|
||||
main {
|
||||
sourceLink {
|
||||
// Unix based directory relative path to the root of the project (where you execute gradle respectively).
|
||||
localDirectory.set(file("src/main/java"))
|
||||
|
||||
// URL showing where the source code can be accessed through the web browser
|
||||
remoteUrl.set(new URL(
|
||||
"https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
||||
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
||||
remoteLineSuffix.set("#L")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 8.2 KiB |
|
@ -21,6 +21,12 @@
|
|||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<package android:name="org.videolan.vlc" />
|
||||
<package android:name="com.instantbits.cast.webvideo" />
|
||||
<package android:name="is.xyz.mpv" />
|
||||
</queries>
|
||||
|
||||
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
|
||||
<application
|
||||
android:name=".AcraApplication"
|
||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.4 KiB |
|
@ -7,10 +7,12 @@ import android.content.ContextWrapper
|
|||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.auto.service.AutoService
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
|
@ -75,20 +77,29 @@ class CustomSenderFactory : ReportSenderFactory {
|
|||
}
|
||||
}
|
||||
|
||||
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.UncaughtExceptionHandler {
|
||||
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||
Thread.UncaughtExceptionHandler {
|
||||
override fun uncaughtException(thread: Thread, error: Throwable) {
|
||||
ACRA.errorReporter.handleException(error)
|
||||
try {
|
||||
PrintStream(errorFile).use { ps ->
|
||||
ps.println(String.format("Enabled resource pack: ${ResourcePackManager.activePackId ?: "none"}"))
|
||||
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
|
||||
ps.println(String.format("Fatal exception on thread %s (%d)", thread.name, thread.id))
|
||||
ps.println(
|
||||
String.format(
|
||||
"Fatal exception on thread %s (%d)",
|
||||
thread.name,
|
||||
thread.id
|
||||
)
|
||||
)
|
||||
error.printStackTrace(ps)
|
||||
}
|
||||
} catch (ignored: FileNotFoundException) { }
|
||||
} catch (ignored: FileNotFoundException) {
|
||||
}
|
||||
try {
|
||||
onError.invoke()
|
||||
} catch (ignored: Exception) { }
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
|
@ -97,7 +108,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U
|
|||
class AcraApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
|
||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||
})
|
||||
|
@ -185,5 +196,15 @@ class AcraApplication : Application() {
|
|||
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
|
||||
context?.openBrowser(url, fallbackWebview, fragment)
|
||||
}
|
||||
|
||||
/** Will fallback to webview if in TV layout */
|
||||
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||
openBrowser(
|
||||
url,
|
||||
isTvSettings(),
|
||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -10,16 +10,22 @@ import android.util.Log
|
|||
import android.view.*
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.gms.cast.framework.CastSession
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
|
||||
|
@ -34,6 +40,7 @@ object CommonActivity {
|
|||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||
}
|
||||
|
||||
|
||||
var canEnterPipMode: Boolean = false
|
||||
var canShowPipMode: Boolean = false
|
||||
var isInPIPMode: Boolean = false
|
||||
|
@ -117,7 +124,7 @@ object CommonActivity {
|
|||
setLocale(this, localeCode)
|
||||
}
|
||||
|
||||
fun init(act: Activity?) {
|
||||
fun init(act: ComponentActivity?) {
|
||||
if (act == null) return
|
||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
||||
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||
|
@ -129,6 +136,22 @@ object CommonActivity {
|
|||
act.updateLocale()
|
||||
act.updateTv()
|
||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||
|
||||
for (resumeApp in resumeApps) {
|
||||
resumeApp.launcher =
|
||||
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val resultCode = result.resultCode
|
||||
val data = result.data
|
||||
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||
val pos = data.getLongExtra(resumeApp.position, -1L)
|
||||
val dur = data.getLongExtra(resumeApp.duration, -1L)
|
||||
if (dur > 0L && pos > 0L)
|
||||
DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
|
||||
removeKey(resumeApp.lastId)
|
||||
ResultFragment.updateUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Activity.enterPIPMode() {
|
||||
|
@ -166,6 +189,8 @@ object CommonActivity {
|
|||
"Light" -> R.style.LightMode
|
||||
"Amoled" -> R.style.AmoledMode
|
||||
"AmoledLight" -> R.style.AmoledModeLight
|
||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.MonetMode else R.style.AppTheme
|
||||
else -> R.style.AppTheme
|
||||
}
|
||||
|
||||
|
@ -186,6 +211,10 @@ object CommonActivity {
|
|||
"Banana" -> R.style.OverlayPrimaryColorBanana
|
||||
"Party" -> R.style.OverlayPrimaryColorParty
|
||||
"Pink" -> R.style.OverlayPrimaryColorPink
|
||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
||||
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
||||
else -> R.style.OverlayPrimaryColorNormal
|
||||
}
|
||||
act.theme.applyStyle(currentTheme, true)
|
||||
|
|
|
@ -40,6 +40,7 @@ object APIHolder {
|
|||
|
||||
private const val defProvider = 0
|
||||
|
||||
// ConcurrentModificationException is possible!!!
|
||||
val allProviders: MutableList<MainAPI> = arrayListOf()
|
||||
|
||||
fun initAll() {
|
||||
|
|
|
@ -11,10 +11,12 @@ import android.view.KeyEvent
|
|||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
|
@ -34,6 +36,8 @@ import com.lagradost.cloudstream3.APIHolder.apis
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.initAll
|
||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
||||
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
||||
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||
|
@ -52,7 +56,6 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri
|
|||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
|
@ -69,10 +72,11 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
|||
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
import com.lagradost.cloudstream3.utils.IOnBackPressed
|
||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||
|
@ -96,17 +100,65 @@ import java.nio.charset.Charset
|
|||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
const val VLC_PACKAGE = "org.videolan.vlc"
|
||||
const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result"
|
||||
val VLC_COMPONENT: ComponentName =
|
||||
ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity")
|
||||
const val VLC_REQUEST_CODE = 42
|
||||
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
|
||||
//https://wiki.videolan.org/Android_Player_Intents/
|
||||
|
||||
const val VLC_FROM_START = -1
|
||||
const val VLC_FROM_PROGRESS = -2
|
||||
const val VLC_EXTRA_POSITION_OUT = "extra_position"
|
||||
const val VLC_EXTRA_DURATION_OUT = "extra_duration"
|
||||
const val VLC_LAST_ID_KEY = "vlc_last_open_id"
|
||||
//https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904
|
||||
//https://mpv-android.github.io/mpv-android/intent.html
|
||||
|
||||
// https://www.webvideocaster.com/integrations
|
||||
|
||||
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
|
||||
|
||||
const val VLC_PACKAGE = "org.videolan.vlc"
|
||||
const val MPV_PACKAGE = "is.xyz.mpv"
|
||||
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
|
||||
|
||||
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
|
||||
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
|
||||
|
||||
//TODO REFACTOR AF
|
||||
data class ResultResume(
|
||||
val packageString: String,
|
||||
val action: String = Intent.ACTION_VIEW,
|
||||
val position: String? = null,
|
||||
val duration: String? = null,
|
||||
var launcher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
val lastId get() = "${packageString}_last_open_id"
|
||||
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
|
||||
val intent = Intent(action)
|
||||
|
||||
if (id != null)
|
||||
setKey(lastId, id)
|
||||
else
|
||||
removeKey(lastId)
|
||||
|
||||
intent.setPackage(packageString)
|
||||
callback.invoke(intent)
|
||||
launcher?.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
val VLC = ResultResume(
|
||||
VLC_PACKAGE,
|
||||
"org.videolan.vlc.player.result",
|
||||
"extra_position",
|
||||
"extra_duration",
|
||||
)
|
||||
|
||||
val MPV = ResultResume(
|
||||
MPV_PACKAGE,
|
||||
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
|
||||
position = "position",
|
||||
duration = "duration",
|
||||
)
|
||||
|
||||
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
|
||||
|
||||
val resumeApps = arrayOf(
|
||||
VLC, MPV, WEB_VIDEO
|
||||
)
|
||||
|
||||
// Short name for requests client to make it nicer to use
|
||||
|
||||
|
@ -152,6 +204,72 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
val mainPluginsLoadedEvent =
|
||||
Event<Boolean>() // homepage api, used to speed up time to load for homepage
|
||||
val afterRepositoryLoadedEvent = Event<Boolean>()
|
||||
|
||||
/**
|
||||
* @return true if the str has launched an app task (be it successful or not)
|
||||
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
|
||||
* */
|
||||
fun handleAppIntentUrl(
|
||||
activity: FragmentActivity?,
|
||||
str: String?,
|
||||
isWebview: Boolean
|
||||
): Boolean =
|
||||
with(activity) {
|
||||
if (str != null && this != null) {
|
||||
if (str.startsWith("https://cs.repo")) {
|
||||
val realUrl = "https://" + str.substringAfter("?")
|
||||
println("Repository url: $realUrl")
|
||||
loadRepository(realUrl)
|
||||
return true
|
||||
} else if (str.contains(appString)) {
|
||||
for (api in OAuth2Apis) {
|
||||
if (str.contains("/${api.redirectUrl}")) {
|
||||
ioSafe {
|
||||
Log.i(TAG, "handleAppIntent $str")
|
||||
val isSuccessful = api.handleRedirect(str)
|
||||
|
||||
if (isSuccessful) {
|
||||
Log.i(TAG, "authenticated ${api.name}")
|
||||
} else {
|
||||
Log.i(TAG, "failed to authenticate ${api.name}")
|
||||
}
|
||||
|
||||
this@with.runOnUiThread {
|
||||
try {
|
||||
showToast(
|
||||
this@with,
|
||||
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
|
||||
api.name
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logError(e) // format might fail
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if (URI(str).scheme == appStringRepo) {
|
||||
val url = str.replaceFirst(appStringRepo, "https")
|
||||
loadRepository(url)
|
||||
return true
|
||||
} else if (!isWebview) {
|
||||
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
|
||||
this.navigate(R.id.navigation_downloads)
|
||||
return true
|
||||
} else {
|
||||
for (api in apis) {
|
||||
if (str.startsWith(api.mainUrl)) {
|
||||
loadResult(str, api.name)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onColorSelected(dialogId: Int, color: Int) {
|
||||
|
@ -313,31 +431,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == VLC_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
val pos: Long =
|
||||
data.getLongExtra(
|
||||
VLC_EXTRA_POSITION_OUT,
|
||||
-1
|
||||
) //Last position in media when player exited
|
||||
val dur: Long =
|
||||
data.getLongExtra(
|
||||
VLC_EXTRA_DURATION_OUT,
|
||||
-1
|
||||
) //Last position in media when player exited
|
||||
val id = getKey<Int>(VLC_LAST_ID_KEY)
|
||||
println("SET KEY $id at $pos / $dur")
|
||||
if (dur > 0 && pos > 0) {
|
||||
setViewPos(id, pos, dur)
|
||||
}
|
||||
removeKey(VLC_LAST_ID_KEY)
|
||||
ResultFragment.updateUI()
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val broadcastIntent = Intent()
|
||||
broadcastIntent.action = "restart_service"
|
||||
|
@ -356,56 +449,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
if (intent == null) return
|
||||
val str = intent.dataString
|
||||
loadCache()
|
||||
if (str != null) {
|
||||
if (str.startsWith("https://cs.repo")) {
|
||||
val realUrl = "https://" + str.substringAfter("?")
|
||||
println("Repository url: $realUrl")
|
||||
loadRepository(realUrl)
|
||||
} else if (str.contains(appString)) {
|
||||
for (api in OAuth2Apis) {
|
||||
if (str.contains("/${api.redirectUrl}")) {
|
||||
val activity = this
|
||||
ioSafe {
|
||||
Log.i(TAG, "handleAppIntent $str")
|
||||
val isSuccessful = api.handleRedirect(str)
|
||||
|
||||
if (isSuccessful) {
|
||||
Log.i(TAG, "authenticated ${api.name}")
|
||||
} else {
|
||||
Log.i(TAG, "failed to authenticate ${api.name}")
|
||||
}
|
||||
|
||||
activity.runOnUiThread {
|
||||
try {
|
||||
showToast(
|
||||
activity,
|
||||
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
|
||||
api.name
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logError(e) // format might fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (URI(str).scheme == appStringRepo) {
|
||||
val url = str.replaceFirst(appStringRepo, "https")
|
||||
loadRepository(url)
|
||||
} else {
|
||||
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
|
||||
this.navigate(R.id.navigation_downloads)
|
||||
} else {
|
||||
for (api in apis) {
|
||||
if (str.startsWith(api.mainUrl)) {
|
||||
loadResult(str, api.name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handleAppIntentUrl(this, str, false)
|
||||
}
|
||||
|
||||
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
|
||||
|
@ -453,7 +497,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
// it.hashCode() is not enough to make sure they are distinct
|
||||
apis = allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
|
||||
apis =
|
||||
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
|
||||
APIHolder.apiMap = null
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
|
@ -478,9 +523,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
lastError = errorFile.readText(Charset.defaultCharset())
|
||||
errorFile.delete()
|
||||
}
|
||||
|
||||
|
||||
val settingsForProvider = SettingsJson()
|
||||
settingsForProvider.enableAdult = settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
|
||||
settingsForProvider.enableAdult =
|
||||
settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
|
||||
|
||||
MainAPI.settingsForProvider = settingsForProvider
|
||||
|
||||
|
@ -514,7 +560,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
|
||||
ioSafe {
|
||||
if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) {
|
||||
if (settingsManager.getBoolean(
|
||||
getString(R.string.auto_update_plugins_key),
|
||||
true
|
||||
)
|
||||
) {
|
||||
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
|
||||
} else {
|
||||
PluginManager.loadAllOnlinePlugins(this@MainActivity)
|
||||
|
@ -558,9 +608,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
for (api in accountManagers) {
|
||||
api.init()
|
||||
}
|
||||
}
|
||||
|
||||
ioSafe {
|
||||
inAppAuths.apmap { api ->
|
||||
try {
|
||||
api.initialize()
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Log
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.base64Decode
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
||||
class AStreamHub : ExtractorApi() {
|
||||
override val name = "AStreamHub"
|
||||
override val mainUrl = "https://astreamhub.com"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
app.get(url).document.selectFirst("body > script").let { script ->
|
||||
val text = script?.html() ?: ""
|
||||
Log.i("Dev", "text => $text")
|
||||
if (text.isNotBlank()) {
|
||||
val m3link = "(?<=file:)(.*)(?=,)".toRegex().find(text)
|
||||
?.groupValues?.get(0)?.trim()?.trim('"') ?: ""
|
||||
Log.i("Dev", "m3link => $m3link")
|
||||
if (m3link.isNotBlank()) {
|
||||
sources.add(
|
||||
ExtractorLink(
|
||||
name = name,
|
||||
source = name,
|
||||
url = m3link,
|
||||
isM3u8 = true,
|
||||
quality = Qualities.Unknown.value,
|
||||
referer = referer ?: url
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
}
|
|
@ -3,10 +3,9 @@ package com.lagradost.cloudstream3.extractors
|
|||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlinx.coroutines.delay
|
||||
import java.net.URI
|
||||
|
||||
class VidSrcExtractor2 : VidSrcExtractor() {
|
||||
override val mainUrl = "https://vidsrc.me/embed"
|
||||
|
@ -27,6 +26,25 @@ open class VidSrcExtractor : ExtractorApi() {
|
|||
override val mainUrl = "$absoluteUrl/embed"
|
||||
override val requiresReferer = false
|
||||
|
||||
companion object {
|
||||
/** Infinite function to validate the vidSrc pass */
|
||||
suspend fun validatePass(url: String) {
|
||||
val uri = URI(url)
|
||||
val host = uri.host
|
||||
|
||||
// Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/
|
||||
val referer = host.split(".").let {
|
||||
val size = it.size
|
||||
"https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/"
|
||||
}
|
||||
|
||||
while (true) {
|
||||
app.get(url, referer = referer)
|
||||
delay(60_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
|
@ -40,7 +58,10 @@ open class VidSrcExtractor : ExtractorApi() {
|
|||
val datahash = it.attr("data-hash")
|
||||
if (datahash.isNotBlank()) {
|
||||
val links = try {
|
||||
app.get("$absoluteUrl/src/$datahash", referer = "https://source.vidsrc.me/").url
|
||||
app.get(
|
||||
"$absoluteUrl/src/$datahash",
|
||||
referer = "https://source.vidsrc.me/"
|
||||
).url
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
|
@ -54,11 +75,28 @@ open class VidSrcExtractor : ExtractorApi() {
|
|||
val srcresponse = app.get(server, referer = absoluteUrl).text
|
||||
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
|
||||
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
srcm3u8,
|
||||
absoluteUrl
|
||||
).forEach(callback)
|
||||
val passRegex = Regex("""['"](.*set_pass[^"']*)""")
|
||||
val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
|
||||
Regex("""^//"""), "https://"
|
||||
)
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
srcm3u8,
|
||||
this.mainUrl,
|
||||
Qualities.Unknown.value,
|
||||
extractorData = pass,
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
|
||||
// M3u8Helper.generateM3u8(
|
||||
// name,
|
||||
// srcm3u8,
|
||||
// absoluteUrl
|
||||
// ).forEach(callback)
|
||||
} else {
|
||||
loadExtractor(linkfixed, url, subtitleCallback, callback)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import kotlin.coroutines.CoroutineContext
|
|||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
const val DEBUG_EXCEPTION = "THIS IS A DEBUG EXCEPTION!"
|
||||
const val DEBUG_PRINT = "DEBUG PRINT"
|
||||
|
||||
class DebugException(message: String) : Exception("$DEBUG_EXCEPTION\n$message")
|
||||
|
||||
|
@ -24,6 +25,12 @@ inline fun debugException(message: () -> String) {
|
|||
}
|
||||
}
|
||||
|
||||
inline fun debugPrint(tag: String = DEBUG_PRINT, message: () -> String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(tag, message.invoke())
|
||||
}
|
||||
}
|
||||
|
||||
inline fun debugWarning(message: () -> String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
logError(DebugException(message.invoke()))
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
package com.lagradost.cloudstream3.plugins
|
||||
|
||||
import android.app.*
|
||||
import dalvik.system.PathClassLoader
|
||||
import com.google.gson.Gson
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Resources
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
|
@ -22,15 +26,18 @@ import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
|||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.extractorApis
|
||||
import com.lagradost.cloudstream3.utils.resources.ResourcePackManager
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.acra.log.debug
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import java.util.*
|
||||
|
@ -39,6 +46,9 @@ import java.util.*
|
|||
const val PLUGINS_KEY = "PLUGINS_KEY"
|
||||
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
|
||||
|
||||
const val EXTENSIONS_CHANNEL_ID = "cloudstream3.extensions"
|
||||
const val EXTENSIONS_CHANNEL_NAME = "Extensions"
|
||||
const val EXTENSIONS_CHANNEL_DESCRIPT = "Extension notification channel"
|
||||
|
||||
// Data class for internal storage
|
||||
data class PluginData(
|
||||
|
@ -79,6 +89,8 @@ object PluginManager {
|
|||
|
||||
const val TAG = "PluginManager"
|
||||
|
||||
private var hasCreatedNotChanel = false
|
||||
|
||||
/**
|
||||
* Store data about the plugin for fetching later
|
||||
* */
|
||||
|
@ -113,6 +125,10 @@ object PluginManager {
|
|||
val plugins = getPluginsOnline().filter {
|
||||
!it.filePath.contains(repositoryPath)
|
||||
}
|
||||
val file = File(repositoryPath)
|
||||
normalSafeApiCall {
|
||||
if (file.exists()) file.deleteRecursively()
|
||||
}
|
||||
setKey(PLUGINS_KEY, plugins)
|
||||
}
|
||||
}
|
||||
|
@ -164,8 +180,16 @@ object PluginManager {
|
|||
val onlineData: Pair<String, SitePlugin>,
|
||||
) {
|
||||
val isOutdated =
|
||||
onlineData.second.version != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
|
||||
onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
|
||||
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN
|
||||
|
||||
fun validOnlineData(context: Context): Boolean {
|
||||
return getPluginPath(
|
||||
context,
|
||||
savedData.internalName,
|
||||
onlineData.first
|
||||
).absolutePath == savedData.filePath
|
||||
}
|
||||
}
|
||||
|
||||
// var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
|
||||
|
@ -211,29 +235,42 @@ object PluginManager {
|
|||
|
||||
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
|
||||
val outdatedPlugins = getPluginsOnline().map { savedData ->
|
||||
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
|
||||
onlinePlugins
|
||||
.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
|
||||
.map { onlineData ->
|
||||
OnlinePluginData(savedData, onlineData)
|
||||
}.filter {
|
||||
it.validOnlineData(activity)
|
||||
}
|
||||
}.flatten().distinctBy { it.onlineData.second.url }
|
||||
|
||||
debug {
|
||||
debugPrint {
|
||||
"Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}"
|
||||
}
|
||||
|
||||
val updatedPlugins = mutableListOf<String>()
|
||||
|
||||
outdatedPlugins.apmap { pluginData ->
|
||||
if (pluginData.isDisabled) {
|
||||
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
|
||||
unloadPlugin(pluginData.savedData.filePath)
|
||||
} else if (pluginData.isOutdated) {
|
||||
downloadAndLoadPlugin(
|
||||
activity,
|
||||
pluginData.onlineData.second.url,
|
||||
pluginData.savedData.internalName,
|
||||
pluginData.onlineData.first
|
||||
)
|
||||
File(pluginData.savedData.filePath)
|
||||
).let { success ->
|
||||
if (success)
|
||||
updatedPlugins.add(pluginData.onlineData.second.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
createNotification(activity, updatedPlugins)
|
||||
}
|
||||
|
||||
ioSafe {
|
||||
afterPluginsLoadedEvent.invoke(true)
|
||||
}
|
||||
|
@ -396,24 +433,61 @@ object PluginManager {
|
|||
) + "." + name.hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* This should not be changed as it is used to also detect if a plugin is installed!
|
||||
**/
|
||||
fun getPluginPath(
|
||||
context: Context,
|
||||
internalName: String,
|
||||
repositoryUrl: String
|
||||
): File {
|
||||
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique
|
||||
val fileName = getPluginSanitizedFileName(internalName)
|
||||
return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for fresh installs
|
||||
* */
|
||||
suspend fun downloadAndLoadPlugin(
|
||||
activity: Activity,
|
||||
pluginUrl: String,
|
||||
internalName: String,
|
||||
repositoryUrl: String
|
||||
): Boolean {
|
||||
try {
|
||||
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique
|
||||
val fileName = getPluginSanitizedFileName(internalName)
|
||||
unloadPlugin("${activity.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
|
||||
val file = getPluginPath(activity, internalName, repositoryUrl)
|
||||
downloadAndLoadPlugin(activity, pluginUrl, internalName, file)
|
||||
return true
|
||||
}
|
||||
|
||||
Log.d(TAG, "Downloading plugin: $pluginUrl to $folderName/$fileName")
|
||||
/**
|
||||
* Used for updates.
|
||||
*
|
||||
* Uses a file instead of repository url, as extensions can get moved it is better to directly
|
||||
* update the files instead of getting the filepath from repo url.
|
||||
* */
|
||||
private suspend fun downloadAndLoadPlugin(
|
||||
activity: Activity,
|
||||
pluginUrl: String,
|
||||
internalName: String,
|
||||
file: File,
|
||||
): Boolean {
|
||||
try {
|
||||
unloadPlugin(file.absolutePath)
|
||||
|
||||
Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}")
|
||||
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
|
||||
val file = downloadPluginToFile(activity, pluginUrl, fileName, folderName)
|
||||
val newFile = downloadPluginToFile(pluginUrl, file)
|
||||
return loadPlugin(
|
||||
activity,
|
||||
file ?: return false,
|
||||
PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET)
|
||||
newFile ?: return false,
|
||||
PluginData(
|
||||
internalName,
|
||||
pluginUrl,
|
||||
true,
|
||||
newFile.absolutePath,
|
||||
PLUGIN_VERSION_NOT_SET
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
|
@ -421,18 +495,13 @@ object PluginManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isFilePath will treat the pluginUrl as as the filepath instead of url
|
||||
* */
|
||||
suspend fun deletePlugin(pluginIdentifier: String, isFilePath: Boolean): Boolean {
|
||||
val data =
|
||||
(if (isFilePath) (getPluginsLocal() + getPluginsOnline()).firstOrNull { it.filePath == pluginIdentifier }
|
||||
else getPluginsOnline().firstOrNull { it.url == pluginIdentifier }) ?: return false
|
||||
suspend fun deletePlugin(file: File): Boolean {
|
||||
val list = (getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
|
||||
|
||||
return try {
|
||||
if (File(data.filePath).delete()) {
|
||||
unloadPlugin(data.filePath)
|
||||
deletePluginData(data)
|
||||
if (File(file.absolutePath).delete()) {
|
||||
unloadPlugin(file.absolutePath)
|
||||
list.forEach { deletePluginData(it) }
|
||||
return true
|
||||
}
|
||||
false
|
||||
|
@ -440,4 +509,63 @@ object PluginManager {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.createNotificationChannel() {
|
||||
hasCreatedNotChanel = true
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name = EXTENSIONS_CHANNEL_NAME //getString(R.string.channel_name)
|
||||
val descriptionText =
|
||||
EXTENSIONS_CHANNEL_DESCRIPT//getString(R.string.channel_description)
|
||||
val importance = NotificationManager.IMPORTANCE_LOW
|
||||
val channel = NotificationChannel(EXTENSIONS_CHANNEL_ID, name, importance).apply {
|
||||
description = descriptionText
|
||||
}
|
||||
// Register the channel with the system
|
||||
val notificationManager: NotificationManager =
|
||||
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(
|
||||
context: Context,
|
||||
extensionNames: List<String>
|
||||
): Notification? {
|
||||
try {
|
||||
if (extensionNames.isEmpty()) return null
|
||||
|
||||
val content = extensionNames.joinToString(", ")
|
||||
// main { // DON'T WANT TO SLOW IT DOWN
|
||||
val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID)
|
||||
.setAutoCancel(false)
|
||||
.setColorized(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSilent(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
||||
.setContentTitle(context.getString(R.string.plugins_updated, extensionNames.size))
|
||||
.setSmallIcon(R.drawable.ic_baseline_extension_24)
|
||||
.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText(content)
|
||||
)
|
||||
.setContentText(content)
|
||||
|
||||
if (!hasCreatedNotChanel) {
|
||||
context.createNotificationChannel()
|
||||
}
|
||||
|
||||
val notification = builder.build()
|
||||
with(NotificationManagerCompat.from(context)) {
|
||||
// notificationId is a unique int for each notification that you must define
|
||||
notify((System.currentTimeMillis() / 1000).toInt(), notification)
|
||||
}
|
||||
return notification
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -84,7 +84,7 @@ object RepositoryManager {
|
|||
// Normal parsed function not working?
|
||||
// return response.parsedSafe()
|
||||
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
|
||||
} catch (t : Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
emptyList()
|
||||
}
|
||||
|
@ -102,9 +102,27 @@ object RepositoryManager {
|
|||
}.flatten()
|
||||
}
|
||||
|
||||
suspend fun downloadPluginToFile(
|
||||
pluginUrl: String,
|
||||
file: File
|
||||
): File? {
|
||||
return suspendSafeApiCall {
|
||||
file.mkdirs()
|
||||
|
||||
// Overwrite if exists
|
||||
if (file.exists()) { file.delete() }
|
||||
file.createNewFile()
|
||||
|
||||
val body = app.get(pluginUrl).okhttpResponse.body
|
||||
write(body.byteStream(), file.outputStream())
|
||||
file
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadPluginToFile(
|
||||
context: Context,
|
||||
pluginUrl: String,
|
||||
/** Filename without .cs3 */
|
||||
fileName: String,
|
||||
folder: String
|
||||
): File? {
|
||||
|
|
|
@ -10,6 +10,9 @@ import java.security.MessageDigest
|
|||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
object VotingApi { // please do not cheat the votes lol
|
||||
private const val LOGKEY = "VotingApi"
|
||||
|
@ -23,10 +26,10 @@ object VotingApi { // please do not cheat the votes lol
|
|||
private val apiDomain = "https://api.countapi.xyz"
|
||||
|
||||
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
||||
MessageDigest
|
||||
.getInstance("SHA-256")
|
||||
.digest("${url}#funny-salt".toByteArray())
|
||||
.fold("") { str, it -> str + "%02x".format(it) }
|
||||
MessageDigest
|
||||
.getInstance("SHA-256")
|
||||
.digest("${url}#funny-salt".toByteArray())
|
||||
.fold("") { str, it -> str + "%02x".format(it) }
|
||||
|
||||
suspend fun SitePlugin.getVotes(): Int {
|
||||
return getVotes(url)
|
||||
|
@ -53,9 +56,9 @@ object VotingApi { // please do not cheat the votes lol
|
|||
return votesCache[pluginUrl] ?: app.get(url).parsedSafe<Result>()?.value?.also {
|
||||
votesCache[pluginUrl] = it
|
||||
} ?: (0.also {
|
||||
ioSafe {
|
||||
createBucket(pluginUrl)
|
||||
}
|
||||
ioSafe {
|
||||
createBucket(pluginUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -64,7 +67,8 @@ object VotingApi { // please do not cheat the votes lol
|
|||
}
|
||||
|
||||
private suspend fun createBucket(pluginUrl: String) {
|
||||
val url = "${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0"
|
||||
val url =
|
||||
"${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0"
|
||||
Log.d(LOGKEY, "Requesting: $url")
|
||||
app.get(url)
|
||||
}
|
||||
|
@ -74,32 +78,46 @@ object VotingApi { // please do not cheat the votes lol
|
|||
return true
|
||||
}
|
||||
|
||||
private val voteLock = Mutex()
|
||||
suspend fun vote(pluginUrl: String, requestType: VoteType): Int {
|
||||
if (!canVote(pluginUrl)) {
|
||||
main {
|
||||
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT).show()
|
||||
// Prevent multiple requests at the same time.
|
||||
voteLock.withLock {
|
||||
if (!canVote(pluginUrl)) {
|
||||
main {
|
||||
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
return getVotes(pluginUrl)
|
||||
}
|
||||
return getVotes(pluginUrl)
|
||||
}
|
||||
val savedType: VoteType = getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
|
||||
var newType: VoteType = requestType
|
||||
var changeValue = 0
|
||||
if (requestType == savedType) {
|
||||
newType = VoteType.NONE
|
||||
changeValue = -requestType.value
|
||||
} else if (savedType == VoteType.NONE) {
|
||||
changeValue = requestType.value
|
||||
} else if (savedType != requestType) {
|
||||
changeValue = -savedType.value + requestType.value
|
||||
}
|
||||
val url = "${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}"
|
||||
Log.d(LOGKEY, "Requesting: $url")
|
||||
val res = app.get(url).parsedSafe<Result>()?.value
|
||||
if (res != null) {
|
||||
|
||||
val savedType: VoteType =
|
||||
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
|
||||
|
||||
val newType = if (requestType == savedType) VoteType.NONE else requestType
|
||||
val changeValue = if (requestType == savedType) {
|
||||
-requestType.value
|
||||
} else if (savedType == VoteType.NONE) {
|
||||
requestType.value
|
||||
} else if (savedType != requestType) {
|
||||
-savedType.value + requestType.value
|
||||
} else 0
|
||||
|
||||
// Pre-emptively set vote key
|
||||
setKey("cs3-votes/${transformUrl(pluginUrl)}", newType)
|
||||
votesCache[pluginUrl] = res
|
||||
|
||||
val url =
|
||||
"${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}"
|
||||
Log.d(LOGKEY, "Requesting: $url")
|
||||
val res = app.get(url).parsedSafe<Result>()?.value
|
||||
|
||||
if (res == null) {
|
||||
// "Refund" key if the response is invalid
|
||||
setKey("cs3-votes/${transformUrl(pluginUrl)}", savedType)
|
||||
} else {
|
||||
votesCache[pluginUrl] = res
|
||||
}
|
||||
return res ?: 0
|
||||
}
|
||||
return res ?: 0
|
||||
}
|
||||
|
||||
private data class Result(
|
||||
|
|
|
@ -37,7 +37,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
|||
val subtitleProviders
|
||||
get() = listOf(
|
||||
openSubtitlesApi,
|
||||
// indexSubtitlesApi // they got anti scraping measures in place :(
|
||||
indexSubtitlesApi // they got anti scraping measures in place :(
|
||||
)
|
||||
|
||||
const val appString = "cloudstreamapp"
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package com.lagradost.cloudstream3.syncproviders
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
interface OAuth2API : AuthAPI {
|
||||
val key: String
|
||||
val redirectUrl: String
|
||||
|
||||
suspend fun handleRedirect(url: String) : Boolean
|
||||
fun authenticate()
|
||||
fun authenticate(activity: FragmentActivity?)
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
|
@ -48,9 +49,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
removeAccountKeys()
|
||||
}
|
||||
|
||||
override fun authenticate() {
|
||||
override fun authenticate(activity: FragmentActivity?) {
|
||||
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
|
||||
openBrowser(request)
|
||||
openBrowser(request, activity)
|
||||
}
|
||||
|
||||
override suspend fun handleRedirect(url: String): Boolean {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
|
||||
|
@ -15,7 +16,7 @@ class Dropbox : OAuth2API {
|
|||
override val icon: Int
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun authenticate() {
|
||||
override fun authenticate(activity: FragmentActivity?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
|
|
@ -20,10 +20,9 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
|
||||
override fun logOut() {}
|
||||
|
||||
private val interceptor = CloudflareKiller()
|
||||
|
||||
companion object {
|
||||
const val host = "https://indexsubtitle.com"
|
||||
const val host = "https://subscene.cyou"
|
||||
const val TAG = "INDEXSUBS"
|
||||
}
|
||||
|
||||
|
@ -126,16 +125,15 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
epNumber = epNum,
|
||||
seasonNumber = seasonNum,
|
||||
year = yearNum,
|
||||
headers = interceptor.getCookieHeaders(link).toMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val document = app.get("$host/?search=$queryText", interceptor = interceptor).document
|
||||
val document = app.get("$host/?search=$queryText").document
|
||||
|
||||
document.select("div.my-3.p-3 div.media").map { block ->
|
||||
if (seasonNum > 0) {
|
||||
val name = block.select("strong.text-primary").text().trim()
|
||||
val name = block.select("strong.text-primary, strong.text-info").text().trim()
|
||||
val season = getOrdinal(seasonNum)
|
||||
if ((block.selectFirst("a")?.attr("href")
|
||||
?.contains(
|
||||
|
@ -163,7 +161,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
val urlItem = fixUrl(
|
||||
it.selectFirst("a")!!.attr("href")
|
||||
)
|
||||
val itemDoc = app.get(urlItem, interceptor = interceptor).document
|
||||
val itemDoc = app.get(urlItem).document
|
||||
val id = imdbUrlToIdNullable(
|
||||
itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent()
|
||||
?.attr("href")
|
||||
|
@ -202,14 +200,14 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
|
||||
|
||||
urlItems.forEach { url ->
|
||||
val request = app.get(url, interceptor = interceptor)
|
||||
val request = app.get(url)
|
||||
if (request.isSuccessful) {
|
||||
request.document.select("div.my-3.p-3 div.media").map { block ->
|
||||
if (block.select("span.d-block span[data-original-title=Language]").text()
|
||||
.trim()
|
||||
.contains("$queryLang")
|
||||
) {
|
||||
var name = block.select("strong.text-primary").text().trim()
|
||||
var name = block.select("strong.text-primary, strong.text-info").text().trim()
|
||||
val link = fixUrl(block.selectFirst("a")!!.attr("href"))
|
||||
if (seasonNum > 0) {
|
||||
when {
|
||||
|
@ -235,7 +233,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
val seasonNum = data.seasonNumber
|
||||
val epNum = data.epNumber
|
||||
|
||||
val req = app.get(data.data, interceptor = interceptor)
|
||||
val req = app.get(data.data)
|
||||
|
||||
if (req.isSuccessful) {
|
||||
val document = req.document
|
||||
|
@ -246,7 +244,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
} else {
|
||||
document.select("div.my-3.p-3 div.media").mapNotNull { block ->
|
||||
val name =
|
||||
block.selectFirst("strong.d-block.text-primary")?.text()?.trim().toString()
|
||||
block.selectFirst("strong.d-block")?.text()?.trim().toString()
|
||||
if (seasonNum!! > 0) {
|
||||
if (isRightEps(name, seasonNum, epNum)) {
|
||||
fixUrl(block.selectFirst("a")!!.attr("href"))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||
|
@ -281,7 +282,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
return false
|
||||
}
|
||||
|
||||
override fun authenticate() {
|
||||
override fun authenticate(activity: FragmentActivity?) {
|
||||
// It is recommended to use a URL-safe string as code_verifier.
|
||||
// See section 4 of RFC 7636 for more details.
|
||||
|
||||
|
@ -294,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
val codeChallenge = codeVerifier
|
||||
val request =
|
||||
"$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
|
||||
openBrowser(request)
|
||||
openBrowser(request, activity)
|
||||
}
|
||||
|
||||
private var requestId = 0
|
||||
|
|
|
@ -11,12 +11,12 @@ import android.webkit.WebViewClient
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
||||
import kotlinx.android.synthetic.main.fragment_webview.*
|
||||
import java.net.URI
|
||||
|
||||
class WebviewFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -31,16 +31,8 @@ class WebviewFragment : Fragment() {
|
|||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
val requestUrl = request?.url.toString()
|
||||
val repoUrl = if (requestUrl.startsWith("https://cs.repo")) {
|
||||
"https://" + requestUrl.substringAfter("?")
|
||||
} else if (URI(requestUrl).scheme == appStringRepo) {
|
||||
requestUrl.replaceFirst(appStringRepo, "https")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (repoUrl != null) {
|
||||
activity?.loadRepository(repoUrl)
|
||||
val performedAction = MainActivity.handleAppIntentUrl(activity, requestUrl, true)
|
||||
if (performedAction) {
|
||||
findNavController().popBackStack()
|
||||
return true
|
||||
}
|
||||
|
@ -48,12 +40,15 @@ class WebviewFragment : Fragment() {
|
|||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
}
|
||||
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
|
||||
web_view.settings.javaScriptEnabled = true
|
||||
web_view.settings.domStorageEnabled = true
|
||||
|
||||
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString
|
||||
// web_view.settings.userAgentString = USER_AGENT
|
||||
|
||||
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
|
||||
web_view.settings.javaScriptEnabled = true
|
||||
web_view.settings.userAgentString = USER_AGENT
|
||||
web_view.settings.domStorageEnabled = true
|
||||
// WebView.setWebContentsDebuggingEnabled(true)
|
||||
|
||||
web_view.loadUrl(url)
|
||||
}
|
||||
|
||||
|
|
|
@ -256,13 +256,14 @@ class HomeFragment : Fragment() {
|
|||
nsfw: MaterialButton?,
|
||||
others: MaterialButton?,
|
||||
): List<Pair<MaterialButton?, List<TvType>>> {
|
||||
// This list should be same order as home screen to aid navigation
|
||||
return listOf(
|
||||
Pair(anime, listOf(TvType.Anime, TvType.OVA, TvType.AnimeMovie)),
|
||||
Pair(cartoons, listOf(TvType.Cartoon)),
|
||||
Pair(tvs, listOf(TvType.TvSeries)),
|
||||
Pair(docs, listOf(TvType.Documentary)),
|
||||
Pair(movies, listOf(TvType.Movie, TvType.Torrent)),
|
||||
Pair(tvs, listOf(TvType.TvSeries)),
|
||||
Pair(anime, listOf(TvType.Anime, TvType.OVA, TvType.AnimeMovie)),
|
||||
Pair(asian, listOf(TvType.AsianDrama)),
|
||||
Pair(cartoons, listOf(TvType.Cartoon)),
|
||||
Pair(docs, listOf(TvType.Documentary)),
|
||||
Pair(livestream, listOf(TvType.Live)),
|
||||
Pair(nsfw, listOf(TvType.NSFW)),
|
||||
Pair(others, listOf(TvType.Others)),
|
||||
|
@ -352,11 +353,25 @@ class HomeFragment : Fragment() {
|
|||
arrayAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Since fire tv is fucked we need to manually define the focus layout.
|
||||
* Since visible buttons are only known in runtime this is required.
|
||||
**/
|
||||
var lastButton: MaterialButton? = null
|
||||
|
||||
for ((button, validTypes) in pairList) {
|
||||
val isValid =
|
||||
validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } }
|
||||
button?.isVisible = isValid
|
||||
if (isValid) {
|
||||
|
||||
// Set focus navigation
|
||||
button?.let { currentButton ->
|
||||
lastButton?.nextFocusRightId = currentButton.id
|
||||
lastButton?.id?.let { currentButton.nextFocusLeftId = it }
|
||||
lastButton = currentButton
|
||||
}
|
||||
|
||||
fun buttonContains(): Boolean {
|
||||
return preSelectedTypes.any { validTypes.contains(it) }
|
||||
}
|
||||
|
@ -506,15 +521,12 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
//Disable Random button, if its toggled off on settings
|
||||
//Load value for toggling Random button. Hide at startup
|
||||
context?.let {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
|
||||
toggleRandomButton =
|
||||
settingsManager.getBoolean(getString(R.string.random_button_key), false)
|
||||
home_random?.isVisible = toggleRandomButton
|
||||
if (!toggleRandomButton) {
|
||||
home_random?.visibility = View.GONE
|
||||
}
|
||||
home_random?.visibility = View.GONE
|
||||
}
|
||||
|
||||
observe(homeViewModel.apiName) { apiName ->
|
||||
|
@ -611,6 +623,7 @@ class HomeFragment : Fragment() {
|
|||
home_loading_shimmer?.stopShimmer()
|
||||
|
||||
val d = data.value
|
||||
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
||||
listHomepageItems.clear()
|
||||
|
||||
// println("ITEMCOUNT: ${d.values.size} ${home_master_recycler?.adapter?.itemCount}")
|
||||
|
@ -623,6 +636,11 @@ class HomeFragment : Fragment() {
|
|||
home_loading_error?.isVisible = false
|
||||
home_loaded?.isVisible = true
|
||||
if (toggleRandomButton) {
|
||||
//Flatten list
|
||||
d.values.forEach { dlist ->
|
||||
mutableListOfResponse.addAll(dlist.list.list)
|
||||
}
|
||||
listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url })
|
||||
home_random?.isVisible = listHomepageItems.isNotEmpty()
|
||||
} else {
|
||||
home_random?.isGone = true
|
||||
|
@ -1016,4 +1034,4 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,9 +276,6 @@ class HomeViewModel : ViewModel() {
|
|||
if (preferredApiName == noneApi.name) {
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
|
||||
loadAndCancel(noneApi)
|
||||
// If the plugin isn't loaded yet. (Does not set the key)
|
||||
} else if (api == null) {
|
||||
loadAndCancel(noneApi)
|
||||
} else if (preferredApiName == randomApi.name) {
|
||||
val validAPIs = context?.filterProviderByPreferredMedia()
|
||||
if (validAPIs.isNullOrEmpty()) {
|
||||
|
@ -289,6 +286,9 @@ class HomeViewModel : ViewModel() {
|
|||
loadAndCancel(apiRandom)
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
|
||||
}
|
||||
// If the plugin isn't loaded yet. (Does not set the key)
|
||||
} else if (api == null) {
|
||||
loadAndCancel(noneApi)
|
||||
} else {
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, api.name)
|
||||
loadAndCancel(api)
|
||||
|
|
|
@ -611,6 +611,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player_lock?.isGone = !isShowing
|
||||
//player_media_route_button?.isClickable = !isGone
|
||||
player_go_back_holder?.isGone = isGone
|
||||
player_sources_btt?.isGone = isGone
|
||||
}
|
||||
|
||||
private fun updateLockUI() {
|
||||
|
|
|
@ -58,6 +58,7 @@ import kotlinx.android.synthetic.main.player_select_source_and_subs.*
|
|||
import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_click_settings
|
||||
import kotlinx.android.synthetic.main.player_select_tracks.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class GeneratorPlayer : FullScreenPlayer() {
|
||||
companion object {
|
||||
|
@ -115,10 +116,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
override fun onTracksInfoChanged() {
|
||||
val tracks = player.getVideoTracks()
|
||||
player_tracks_btt?.isVisible = tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1
|
||||
player_tracks_btt?.isVisible =
|
||||
tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1
|
||||
// Only set the preferred language if it is available.
|
||||
// Otherwise it may give some users audio track init failed!
|
||||
if (tracks.allAudioTracks.any { it.language == preferredAudioTrackLanguage }){
|
||||
if (tracks.allAudioTracks.any { it.language == preferredAudioTrackLanguage }) {
|
||||
player.setPreferredAudioTrack(preferredAudioTrackLanguage)
|
||||
}
|
||||
}
|
||||
|
@ -602,8 +604,20 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
subtitleList.setItemChecked(subtitleIndex, true)
|
||||
|
||||
subtitleList.setOnItemClickListener { _, _, which, _ ->
|
||||
subtitleIndex = which
|
||||
subtitleList.setItemChecked(which, true)
|
||||
if (which > currentSubtitles.size) {
|
||||
// Since android TV is funky the setOnItemClickListener will be triggered
|
||||
// instead of setOnClickListener when selecting. To override this we programmatically
|
||||
// click the view when selecting an item outside the list.
|
||||
|
||||
// Cheeky way of getting the view at that position to click it
|
||||
// to avoid keeping track of the various footers.
|
||||
// getChildAt() gives null :(
|
||||
val child = subtitleList.adapter.getView(which, null, subtitleList)
|
||||
child?.performClick()
|
||||
} else {
|
||||
subtitleIndex = which
|
||||
subtitleList.setItemChecked(which, true)
|
||||
}
|
||||
}
|
||||
|
||||
sourceDialog.cancel_btt?.setOnClickListener {
|
||||
|
@ -762,7 +776,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
// audioArrayAdapter.add(ctx.getString(R.string.no_subtitles))
|
||||
audioArrayAdapter.addAll(currentAudioTracks.mapIndexed { index, format ->
|
||||
format.label ?: format.language?.let { fromTwoLettersToLanguage(it) } ?: index.toString()
|
||||
format.label ?: format.language?.let { fromTwoLettersToLanguage(it) }
|
||||
?: index.toString()
|
||||
})
|
||||
|
||||
audioList.adapter = audioArrayAdapter
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.ui.result
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -9,6 +10,7 @@ import android.widget.TextView
|
|||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
@ -53,6 +55,10 @@ const val ACTION_SHOW_DESCRIPTION = 15
|
|||
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13
|
||||
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
|
||||
|
||||
const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16
|
||||
const val ACTION_PLAY_EPISODE_IN_MPV = 17
|
||||
|
||||
|
||||
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
|
||||
|
||||
class EpisodeAdapter(
|
||||
|
@ -60,7 +66,26 @@ class EpisodeAdapter(
|
|||
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
||||
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var cardList: MutableList<ResultEpisode> = mutableListOf()
|
||||
companion object {
|
||||
/**
|
||||
* @return ACTION_PLAY_EPISODE_IN_PLAYER, ACTION_PLAY_EPISODE_IN_BROWSER or ACTION_PLAY_EPISODE_IN_VLC_PLAYER depending on player settings.
|
||||
* See array.xml/player_pref_values
|
||||
**/
|
||||
fun getPlayerAction(context: Context): Int {
|
||||
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return when (settingsManager.getInt(context.getString(R.string.player_pref_key), 1)) {
|
||||
1 -> ACTION_PLAY_EPISODE_IN_PLAYER
|
||||
2 -> ACTION_PLAY_EPISODE_IN_VLC_PLAYER
|
||||
3 -> ACTION_PLAY_EPISODE_IN_BROWSER
|
||||
4 -> ACTION_PLAY_EPISODE_IN_WEB_VIDEO
|
||||
5 -> ACTION_PLAY_EPISODE_IN_MPV
|
||||
else -> ACTION_PLAY_EPISODE_IN_PLAYER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cardList: MutableList<ResultEpisode> = mutableListOf()
|
||||
|
||||
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
|
||||
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
|
||||
|
@ -239,7 +264,6 @@ class EpisodeAdapter(
|
|||
|
||||
itemView.setOnLongClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
|
||||
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
|||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
|
||||
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
@ -95,6 +96,7 @@ import kotlinx.android.synthetic.main.fragment_result.result_vpn
|
|||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||
import kotlinx.android.synthetic.main.fragment_result_tv.*
|
||||
import kotlinx.android.synthetic.main.result_sync.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
||||
|
@ -293,7 +295,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
result_reload_connection_open_in_browser?.isVisible = true
|
||||
}
|
||||
2 -> {
|
||||
result_bookmark_fab?.isGone = isTvSettings()
|
||||
result_bookmark_fab?.isGone = isTrueTvSettings()
|
||||
result_bookmark_fab?.extend()
|
||||
//if (result_bookmark_button?.context?.isTrueTvSettings() == true) {
|
||||
// when {
|
||||
|
@ -412,7 +414,39 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
is ResourceSome.Success -> {
|
||||
result_episodes?.isVisible = true
|
||||
result_episode_loading?.isVisible = false
|
||||
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
// Do not use this.isTv, that is the player
|
||||
val isTv = isTvSettings()
|
||||
val hasEpisodes =
|
||||
!(result_episodes?.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty()
|
||||
|
||||
if (isTv && hasEpisodes) {
|
||||
// Make it impossible to focus anywhere else!
|
||||
temporary_no_focus?.isFocusable = true
|
||||
temporary_no_focus?.requestFocus()
|
||||
}
|
||||
|
||||
(result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value)
|
||||
|
||||
if (isTv && hasEpisodes) main {
|
||||
delay(500)
|
||||
temporary_no_focus?.isFocusable = false
|
||||
// This might make some people sad as it changes the focus when leaving an episode :(
|
||||
result_episodes?.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -422,7 +456,8 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
val apiName: String,
|
||||
val showFillers: Boolean,
|
||||
val dubStatus: DubStatus,
|
||||
val start: AutoResume?
|
||||
val start: AutoResume?,
|
||||
val playerAction: Int
|
||||
)
|
||||
|
||||
private fun getStoredData(context: Context): StoredData? {
|
||||
|
@ -436,6 +471,8 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
) DubStatus.Dubbed else DubStatus.Subbed
|
||||
val startAction = arguments?.getInt(START_ACTION_BUNDLE)
|
||||
|
||||
val playerAction = getPlayerAction(context)
|
||||
|
||||
val start = startAction?.let { action ->
|
||||
val startValue = arguments?.getInt(START_VALUE_BUNDLE)
|
||||
val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE)
|
||||
|
@ -450,7 +487,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
season = resumeSeason
|
||||
)
|
||||
}
|
||||
return StoredData(url, apiName, showFillers, dubStatus, start)
|
||||
return StoredData(url, apiName, showFillers, dubStatus, start, playerAction)
|
||||
}
|
||||
|
||||
private fun reloadViewModel(success: Boolean = false) {
|
||||
|
@ -458,7 +495,14 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
val storedData = getStoredData(activity ?: context ?: return) ?: return
|
||||
|
||||
//viewModel.clear()
|
||||
viewModel.load(activity, storedData.url ?: return, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start)
|
||||
viewModel.load(
|
||||
activity,
|
||||
storedData.url ?: return,
|
||||
storedData.apiName,
|
||||
storedData.showFillers,
|
||||
storedData.dubStatus,
|
||||
storedData.start
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -734,7 +778,8 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
viewModel.handleAction(
|
||||
activity,
|
||||
EpisodeClickEvent(
|
||||
ACTION_PLAY_EPISODE_IN_PLAYER, value.result
|
||||
storedData?.playerAction ?: ACTION_PLAY_EPISODE_IN_PLAYER,
|
||||
value.result
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -916,7 +961,14 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
|
||||
if (storedData?.url != null) {
|
||||
result_reload_connectionerror.setOnClickListener {
|
||||
viewModel.load(activity, storedData.url, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start)
|
||||
viewModel.load(
|
||||
activity,
|
||||
storedData.url,
|
||||
storedData.apiName,
|
||||
storedData.showFillers,
|
||||
storedData.dubStatus,
|
||||
storedData.start
|
||||
)
|
||||
}
|
||||
|
||||
result_reload_connection_open_in_browser?.setOnClickListener {
|
||||
|
@ -952,7 +1004,14 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
|
||||
if (restart || !viewModel.hasLoaded()) {
|
||||
//viewModel.clear()
|
||||
viewModel.load(activity, storedData.url, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start)
|
||||
viewModel.load(
|
||||
activity,
|
||||
storedData.url,
|
||||
storedData.apiName,
|
||||
storedData.showFillers,
|
||||
storedData.dubStatus,
|
||||
storedData.start
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package com.lagradost.cloudstream3.ui.result
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
@ -16,6 +15,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getId
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
|
@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
|||
import com.lagradost.cloudstream3.ui.player.IGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
|
@ -43,7 +44,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
|||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
|
||||
|
@ -52,9 +52,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
|||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.lang.Math.abs
|
||||
|
@ -615,7 +613,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let
|
||||
|
||||
// SET VISUAL KEYS
|
||||
AcraApplication.setKey(
|
||||
setKey(
|
||||
DOWNLOAD_HEADER_CACHE,
|
||||
parentId.toString(),
|
||||
VideoDownloadHelper.DownloadHeaderCached(
|
||||
|
@ -629,7 +627,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
|
||||
AcraApplication.setKey(
|
||||
setKey(
|
||||
DataStore.getFolderName(
|
||||
DOWNLOAD_EPISODE_CACHE,
|
||||
parentId.toString()
|
||||
|
@ -956,70 +954,155 @@ class ResultViewModel2 : ViewModel() {
|
|||
return LinkLoadingResult(sortUrls(links), sortSubs(subs))
|
||||
}
|
||||
|
||||
private fun playWithVlc(act: Activity?, data: LinkLoadingResult, id: Int) = ioSafe {
|
||||
if (act == null) return@ioSafe
|
||||
if (data.links.isEmpty()) {
|
||||
showToast(act, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
return@ioSafe
|
||||
private fun launchActivity(
|
||||
activity: Activity?,
|
||||
resumeApp: ResultResume,
|
||||
id: Int? = null,
|
||||
work: suspend (Intent.(Activity) -> Unit)
|
||||
): Job? {
|
||||
val act = activity ?: return null
|
||||
return CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
resumeApp.launch(id) {
|
||||
work(act)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
main {
|
||||
if (t is ActivityNotFoundException) {
|
||||
showToast(activity, txt(R.string.app_not_found_error), Toast.LENGTH_LONG)
|
||||
} else {
|
||||
showToast(activity, t.toString(), Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!act.checkWrite()) {
|
||||
act.requestRW()
|
||||
if (act.checkWrite()) return@ioSafe
|
||||
}
|
||||
}
|
||||
|
||||
val outputDir = act.cacheDir
|
||||
val outputFile = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("mirrorlist", ".m3u8", outputDir)
|
||||
private fun playInWebVideo(
|
||||
activity: Activity?,
|
||||
link: ExtractorLink,
|
||||
title: String?,
|
||||
posterUrl: String?,
|
||||
subtitles: List<SubtitleData>
|
||||
) = launchActivity(activity, WEB_VIDEO) {
|
||||
setDataAndType(Uri.parse(link.url), "video/*")
|
||||
|
||||
putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray())
|
||||
title?.let { putExtra("title", title) }
|
||||
posterUrl?.let { putExtra("poster", posterUrl) }
|
||||
val headers = Bundle().apply {
|
||||
if (link.referer.isNotBlank())
|
||||
putString("Referer", link.referer)
|
||||
putString("User-Agent", USER_AGENT)
|
||||
for ((key, value) in link.headers) {
|
||||
putString(key, value)
|
||||
}
|
||||
}
|
||||
putExtra("android.media.intent.extra.HTTP_HEADERS", headers)
|
||||
putExtra("secure_uri", true)
|
||||
}
|
||||
|
||||
private fun playWithMpv(
|
||||
activity: Activity?,
|
||||
id: Int,
|
||||
link: ExtractorLink,
|
||||
subtitles: List<SubtitleData>,
|
||||
resume: Boolean = true,
|
||||
) = launchActivity(activity, MPV, id) {
|
||||
putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray())
|
||||
putExtra("subs.name", subtitles.map { it.name }.toTypedArray())
|
||||
putExtra("subs.filename", subtitles.map { it.name }.toTypedArray())
|
||||
setDataAndType(Uri.parse(link.url), "video/*")
|
||||
component = MPV_COMPONENT
|
||||
putExtra("secure_uri", true)
|
||||
putExtra("return_result", true)
|
||||
val position = getViewPos(id)?.position
|
||||
if (resume && position != null)
|
||||
putExtra("position", position.toInt())
|
||||
}
|
||||
|
||||
// https://wiki.videolan.org/Android_Player_Intents/
|
||||
private fun playWithVlc(
|
||||
activity: Activity?,
|
||||
data: LinkLoadingResult,
|
||||
id: Int,
|
||||
resume: Boolean = true,
|
||||
// if it is only a single link then resume works correctly
|
||||
singleFile: Boolean? = null
|
||||
) = launchActivity(activity, VLC, id) { act ->
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
|
||||
val outputDir = act.cacheDir
|
||||
|
||||
if (singleFile ?: (data.links.size == 1)) {
|
||||
setDataAndType(data.links.first().url.toUri(), "video/*")
|
||||
} else {
|
||||
val outputFile = File.createTempFile("mirrorlist", ".m3u8", outputDir)
|
||||
|
||||
var text = "#EXTM3U"
|
||||
for (sub in data.subs) {
|
||||
text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\""
|
||||
}
|
||||
|
||||
// With subtitles it doesn't work for no reason :(
|
||||
// for (sub in data.subs) {
|
||||
// text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\""
|
||||
// }
|
||||
for (link in data.links) {
|
||||
text += "\n#EXTINF:, ${link.name}\n${link.url}"
|
||||
}
|
||||
outputFile.writeText(text)
|
||||
|
||||
val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT)
|
||||
|
||||
vlcIntent.setPackage(VLC_PACKAGE)
|
||||
vlcIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
vlcIntent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
vlcIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
vlcIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
|
||||
vlcIntent.setDataAndType(
|
||||
setDataAndType(
|
||||
FileProvider.getUriForFile(
|
||||
act,
|
||||
act.applicationContext.packageName + ".provider",
|
||||
outputFile
|
||||
), "video/*"
|
||||
)
|
||||
|
||||
val startId = VLC_FROM_PROGRESS
|
||||
|
||||
var position = startId
|
||||
if (startId == VLC_FROM_START) {
|
||||
position = 1
|
||||
} else if (startId == VLC_FROM_PROGRESS) {
|
||||
position = 0
|
||||
}
|
||||
|
||||
vlcIntent.putExtra("position", position)
|
||||
|
||||
vlcIntent.component = VLC_COMPONENT
|
||||
act.setKey(VLC_LAST_ID_KEY, id)
|
||||
act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
showToast(act, e.toString(), Toast.LENGTH_LONG)
|
||||
}
|
||||
|
||||
val position = if (resume) {
|
||||
getViewPos(id)?.position ?: 0L
|
||||
} else {
|
||||
1L
|
||||
}
|
||||
|
||||
component = VLC_COMPONENT
|
||||
|
||||
putExtra("from_start", !resume)
|
||||
putExtra("position", position)
|
||||
}
|
||||
|
||||
fun handleAction(activity: Activity?, click: EpisodeClickEvent) = viewModelScope.launchSafe {
|
||||
handleEpisodeClickEvent(activity, click)
|
||||
}
|
||||
|
||||
fun handleAction(activity: Activity?, click: EpisodeClickEvent) =
|
||||
viewModelScope.launchSafe {
|
||||
handleEpisodeClickEvent(activity, click)
|
||||
}
|
||||
|
||||
data class ExternalApp(
|
||||
val packageString: String,
|
||||
val name: Int,
|
||||
val action: Int,
|
||||
)
|
||||
|
||||
private val apps = listOf(
|
||||
ExternalApp(
|
||||
VLC_PACKAGE,
|
||||
R.string.player_settings_play_in_vlc,
|
||||
ACTION_PLAY_EPISODE_IN_VLC_PLAYER
|
||||
), ExternalApp(
|
||||
WEB_VIDEO_CAST_PACKAGE,
|
||||
R.string.player_settings_play_in_web,
|
||||
ACTION_PLAY_EPISODE_IN_WEB_VIDEO
|
||||
),
|
||||
ExternalApp(
|
||||
MPV_PACKAGE,
|
||||
R.string.player_settings_play_in_mpv,
|
||||
ACTION_PLAY_EPISODE_IN_MPV
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) {
|
||||
when (click.action) {
|
||||
|
@ -1035,9 +1118,17 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER)
|
||||
|
||||
if (activity?.isAppInstalled(VLC_PACKAGE) == true) {
|
||||
options.add(txt(R.string.episode_action_play_in_vlc) to ACTION_PLAY_EPISODE_IN_VLC_PLAYER)
|
||||
for (app in apps) {
|
||||
if (activity?.isAppInstalled(app.packageString) == true) {
|
||||
options.add(
|
||||
txt(
|
||||
R.string.episode_action_play_in_format,
|
||||
txt(app.name)
|
||||
) to app.action
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
options.addAll(
|
||||
listOf(
|
||||
txt(R.string.episode_action_play_in_browser) to ACTION_PLAY_EPISODE_IN_BROWSER,
|
||||
|
@ -1073,9 +1164,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
click.copy(action = ACTION_CHROME_CAST_EPISODE)
|
||||
)
|
||||
} else {
|
||||
val action = getPlayerAction(ctx)
|
||||
handleEpisodeClickEvent(
|
||||
activity,
|
||||
click.copy(action = ACTION_PLAY_EPISODE_IN_PLAYER)
|
||||
click.copy(action = action)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1212,6 +1304,11 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
|
||||
loadLinks(click.data, isVisible = true, isCasting = true) { links ->
|
||||
if (links.links.isEmpty()) {
|
||||
showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
return@loadLinks
|
||||
}
|
||||
|
||||
playWithVlc(
|
||||
activity,
|
||||
links,
|
||||
|
@ -1219,6 +1316,37 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
}
|
||||
}
|
||||
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
txt(
|
||||
R.string.episode_action_play_in_format,
|
||||
txt(R.string.player_settings_play_in_web)
|
||||
)
|
||||
) { (result, index) ->
|
||||
playInWebVideo(
|
||||
activity,
|
||||
result.links[index],
|
||||
click.data.name ?: click.data.headerName,
|
||||
click.data.poster,
|
||||
result.subs
|
||||
)
|
||||
}
|
||||
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
txt(
|
||||
R.string.episode_action_play_in_format,
|
||||
txt(R.string.player_settings_play_in_mpv)
|
||||
)
|
||||
) { (result, index) ->
|
||||
playWithMpv(
|
||||
activity,
|
||||
click.data.id,
|
||||
result.links[index],
|
||||
result.subs
|
||||
)
|
||||
}
|
||||
ACTION_PLAY_EPISODE_IN_PLAYER -> {
|
||||
val data = currentResponse?.syncData?.toList() ?: emptyList()
|
||||
val list =
|
||||
|
@ -1284,7 +1412,11 @@ class ResultViewModel2 : ViewModel() {
|
|||
}, {
|
||||
if (this !is AnimeLoadResponse) return@argamap
|
||||
val map =
|
||||
Kitsu.getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false)
|
||||
Kitsu.getEpisodesDetails(
|
||||
getMalId(),
|
||||
getAniListId(),
|
||||
isResponseRequired = false
|
||||
)
|
||||
if (map.isNullOrEmpty()) return@argamap
|
||||
updateEpisodes = DubStatus.values().map { dubStatus ->
|
||||
val current =
|
||||
|
@ -1304,8 +1436,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
val currentBack = this
|
||||
this.description = this.description ?: node.description?.en
|
||||
this.name = this.name ?: node.titles?.canonical
|
||||
this.episode = this.episode ?: node.num ?: episodeNumbers[index]
|
||||
this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url
|
||||
this.episode =
|
||||
this.episode ?: node.num ?: episodeNumbers[index]
|
||||
this.posterUrl =
|
||||
this.posterUrl ?: node.thumbnail?.original?.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1592,7 +1726,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
val idIndex = ep.key.id
|
||||
for ((index, i) in ep.value.withIndex()) {
|
||||
val episode = i.episode ?: (index + 1)
|
||||
val id = mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) ?: 0)
|
||||
val id =
|
||||
mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000)
|
||||
?: 0)
|
||||
if (!existingEpisodes.contains(id)) {
|
||||
existingEpisodes.add(id)
|
||||
val seasonData = loadResponse.seasonNames.getSeason(i.season)
|
||||
|
@ -1888,7 +2024,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
if (ep.getWatchProgress() > 0.9) continue
|
||||
handleAction(
|
||||
activity,
|
||||
EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep)
|
||||
EpisodeClickEvent(
|
||||
getPlayerAction(activity),
|
||||
ep
|
||||
)
|
||||
)
|
||||
break
|
||||
}
|
||||
|
@ -1905,7 +2044,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
?: return@launchSafe
|
||||
handleAction(
|
||||
activity,
|
||||
EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, episode)
|
||||
EpisodeClickEvent(
|
||||
getPlayerAction(activity),
|
||||
episode
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1983,7 +2125,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
preferStartEpisode = getResultEpisode(mainId)
|
||||
preferStartSeason = getResultSeason(mainId)
|
||||
|
||||
AcraApplication.setKey(
|
||||
setKey(
|
||||
DOWNLOAD_HEADER_CACHE,
|
||||
mainId.toString(),
|
||||
VideoDownloadHelper.DownloadHeaderCached(
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.View.*
|
||||
|
@ -12,8 +9,10 @@ import androidx.annotation.UiThread
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -39,7 +38,11 @@ import kotlinx.android.synthetic.main.add_account_input.*
|
|||
class SettingsAccount : PreferenceFragmentCompat() {
|
||||
companion object {
|
||||
/** Used by nginx plugin too */
|
||||
fun showLoginInfo(activity: Activity?, api: AccountManager, info: AuthAPI.LoginInfo) {
|
||||
fun showLoginInfo(
|
||||
activity: FragmentActivity?,
|
||||
api: AccountManager,
|
||||
info: AuthAPI.LoginInfo
|
||||
) {
|
||||
val builder =
|
||||
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.account_managment)
|
||||
|
@ -62,9 +65,13 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
dialog.dismissSafe(activity)
|
||||
showAccountSwitch(activity, api)
|
||||
}
|
||||
|
||||
if (isTvSettings()) {
|
||||
dialog.account_switch_account?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
fun showAccountSwitch(activity: Activity, api: AccountManager) {
|
||||
fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) {
|
||||
val accounts = api.getAccounts() ?: return
|
||||
|
||||
val builder =
|
||||
|
@ -98,11 +105,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
@UiThread
|
||||
fun addAccount(activity: Activity?, api: AccountManager) {
|
||||
fun addAccount(activity: FragmentActivity?, api: AccountManager) {
|
||||
try {
|
||||
when (api) {
|
||||
is OAuth2API -> {
|
||||
api.authenticate()
|
||||
api.authenticate(activity)
|
||||
}
|
||||
is InAppAuthAPI -> {
|
||||
val builder =
|
||||
|
@ -144,13 +151,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
dialog.login_username_input?.isVisible = api.requiresUsername
|
||||
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank()
|
||||
dialog.create_account?.setOnClickListener {
|
||||
val i = Intent(Intent.ACTION_VIEW)
|
||||
i.data = Uri.parse(api.createAccountUrl)
|
||||
try {
|
||||
activity.startActivity(i)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
openBrowser(
|
||||
api.createAccountUrl ?: return@setOnClickListener,
|
||||
activity
|
||||
)
|
||||
dialog.dismissSafe()
|
||||
}
|
||||
dialog.text1?.text = api.name
|
||||
|
||||
|
@ -181,9 +186,10 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
try {
|
||||
showToast(
|
||||
activity,
|
||||
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
|
||||
api.name
|
||||
)
|
||||
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail)
|
||||
.format(
|
||||
api.name
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logError(e) // format might fail
|
||||
|
|
|
@ -113,6 +113,22 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.player_pref_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.player_pref_names)
|
||||
val prefValues = resources.getIntArray(R.array.player_pref_values)
|
||||
val current = settingsManager.getInt(getString(R.string.player_pref_key), 1)
|
||||
|
||||
activity?.showBottomDialog(
|
||||
prefNames.toList(),
|
||||
prefValues.indexOf(current),
|
||||
getString(R.string.player_pref),
|
||||
true,
|
||||
{}) {
|
||||
settingsManager.edit().putInt(getString(R.string.player_pref_key), prefValues[it]).apply()
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.subtitle_settings_key)?.setOnPreferenceClickListener {
|
||||
SubtitlesFragment.push(activity, false)
|
||||
return@setOnPreferenceClickListener true
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
|
@ -86,8 +87,20 @@ class SettingsUI : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
getPref(R.string.app_theme_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.themes_names)
|
||||
val prefValues = resources.getStringArray(R.array.themes_names_values)
|
||||
val prefNames = resources.getStringArray(R.array.themes_names).toMutableList()
|
||||
val prefValues = resources.getStringArray(R.array.themes_names_values).toMutableList()
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less
|
||||
val toRemove = prefValues
|
||||
.mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null }
|
||||
.filterNotNull()
|
||||
var offset = 0
|
||||
toRemove.forEach { idx ->
|
||||
prefNames.removeAt(idx - offset)
|
||||
prefValues.removeAt(idx - offset)
|
||||
offset += 1
|
||||
}
|
||||
}
|
||||
|
||||
val currentLayout =
|
||||
settingsManager.getString(getString(R.string.app_theme_key), prefValues.first())
|
||||
|
@ -110,8 +123,20 @@ class SettingsUI : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
getPref(R.string.primary_color_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.themes_overlay_names)
|
||||
val prefValues = resources.getStringArray(R.array.themes_overlay_names_values)
|
||||
val prefNames = resources.getStringArray(R.array.themes_overlay_names).toMutableList()
|
||||
val prefValues = resources.getStringArray(R.array.themes_overlay_names_values).toMutableList()
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less
|
||||
val toRemove = prefValues
|
||||
.mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null }
|
||||
.filterNotNull()
|
||||
var offset = 0
|
||||
toRemove.forEach { idx ->
|
||||
prefNames.removeAt(idx - offset)
|
||||
prefValues.removeAt(idx - offset)
|
||||
offset += 1
|
||||
}
|
||||
}
|
||||
|
||||
val currentLayout =
|
||||
settingsManager.getString(getString(R.string.primary_color_key), prefValues.first())
|
||||
|
|
|
@ -3,21 +3,19 @@ package com.lagradost.cloudstream3.ui.settings.extensions
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
import com.lagradost.cloudstream3.mvvm.Some
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import kotlinx.coroutines.launch
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
|
||||
data class RepositoryData(
|
||||
@JsonProperty("name") val name: String,
|
||||
|
@ -46,7 +44,8 @@ class ExtensionsViewModel : ViewModel() {
|
|||
val pluginStats: LiveData<Some<PluginStats>> = _pluginStats
|
||||
|
||||
//TODO CACHE GET REQUESTS
|
||||
fun loadStats() = viewModelScope.launchSafe {
|
||||
// DO not use viewModelScope.launchSafe, it will ANR on slow internet
|
||||
fun loadStats() = ioSafe {
|
||||
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
||||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||
|
||||
|
@ -61,6 +60,7 @@ class ExtensionsViewModel : ViewModel() {
|
|||
PluginManager.OnlinePluginData(savedData, onlineData)
|
||||
}
|
||||
}.flatten().distinctBy { it.onlineData.second.url }
|
||||
|
||||
val total = onlinePlugins.count()
|
||||
val disabled = outdatedPlugins.count { it.isDisabled }
|
||||
val downloadedTotal = outdatedPlugins.count()
|
||||
|
|
|
@ -147,7 +147,7 @@ class PluginsFragment : Fragment() {
|
|||
pluginViewModel.updatePluginListLocal()
|
||||
tv_types_scroll_view?.isVisible = false
|
||||
} else {
|
||||
pluginViewModel.updatePluginList(url)
|
||||
pluginViewModel.updatePluginList(context, url)
|
||||
tv_types_scroll_view?.isVisible = true
|
||||
|
||||
// 💀💀💀💀💀💀💀 Recyclerview when
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
|
@ -13,6 +14,7 @@ import com.lagradost.cloudstream3.apmap
|
|||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.plugins.PluginData
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.plugins.SitePlugin
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
|
@ -45,8 +47,8 @@ class PluginsViewModel : ViewModel() {
|
|||
private val repositoryCache: MutableMap<String, List<Plugin>> = mutableMapOf()
|
||||
const val TAG = "PLG"
|
||||
|
||||
private fun isDownloaded(plugin: Plugin, data: Set<String>? = null): Boolean {
|
||||
return (data ?: getDownloads()).contains(plugin.second.internalName)
|
||||
private fun isDownloaded(context: Context, pluginName: String, repositoryUrl: String): Boolean {
|
||||
return getPluginPath(context, pluginName, repositoryUrl).exists()
|
||||
}
|
||||
|
||||
private suspend fun getPlugins(
|
||||
|
@ -63,24 +65,15 @@ class PluginsViewModel : ViewModel() {
|
|||
?.also { repositoryCache[repositoryUrl] = it } ?: emptyList()
|
||||
}
|
||||
|
||||
private fun getStoredPlugins(): Array<PluginData> {
|
||||
return PluginManager.getPluginsOnline()
|
||||
}
|
||||
|
||||
private fun getDownloads(): Set<String> {
|
||||
return getStoredPlugins().map { it.internalName }.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param viewModel optional, updates the plugins livedata for that viewModel if included
|
||||
* */
|
||||
fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) =
|
||||
ioSafe {
|
||||
if (activity == null) return@ioSafe
|
||||
val stored = getDownloads()
|
||||
val plugins = getPlugins(repositoryUrl)
|
||||
|
||||
plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list ->
|
||||
plugins.filter { plugin -> !isDownloaded(activity, plugin.second.internalName, repositoryUrl) }.also { list ->
|
||||
main {
|
||||
showToast(
|
||||
activity,
|
||||
|
@ -103,7 +96,7 @@ class PluginsViewModel : ViewModel() {
|
|||
PluginManager.downloadAndLoadPlugin(
|
||||
activity,
|
||||
metadata.url,
|
||||
metadata.name,
|
||||
metadata.internalName,
|
||||
repo
|
||||
)
|
||||
}.main { list ->
|
||||
|
@ -117,7 +110,7 @@ class PluginsViewModel : ViewModel() {
|
|||
),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
viewModel?.updatePluginListPrivate(repositoryUrl)
|
||||
viewModel?.updatePluginListPrivate(activity, repositoryUrl)
|
||||
} else if (list.isNotEmpty()) {
|
||||
showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
@ -140,11 +133,10 @@ class PluginsViewModel : ViewModel() {
|
|||
if (activity == null) return@ioSafe
|
||||
val (repo, metadata) = plugin
|
||||
|
||||
val (success, message) = if (isDownloaded(plugin) || isLocal) {
|
||||
PluginManager.deletePlugin(
|
||||
metadata.url,
|
||||
isLocal
|
||||
) to R.string.plugin_deleted
|
||||
val file = getPluginPath(activity, plugin.second.internalName, plugin.first)
|
||||
|
||||
val (success, message) = if (file.exists() || isLocal) {
|
||||
PluginManager.deletePlugin(file) to R.string.plugin_deleted
|
||||
} else {
|
||||
PluginManager.downloadAndLoadPlugin(
|
||||
activity,
|
||||
|
@ -165,14 +157,13 @@ class PluginsViewModel : ViewModel() {
|
|||
if (isLocal)
|
||||
updatePluginListLocal()
|
||||
else
|
||||
updatePluginListPrivate(repositoryUrl)
|
||||
updatePluginListPrivate(activity, repositoryUrl)
|
||||
}
|
||||
|
||||
private suspend fun updatePluginListPrivate(repositoryUrl: String) {
|
||||
val stored = getDownloads()
|
||||
private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) {
|
||||
val plugins = getPlugins(repositoryUrl)
|
||||
val list = plugins.map { plugin ->
|
||||
PluginViewData(plugin, isDownloaded(plugin, stored))
|
||||
PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first))
|
||||
}
|
||||
|
||||
this.plugins = list
|
||||
|
@ -211,9 +202,10 @@ class PluginsViewModel : ViewModel() {
|
|||
_filteredPlugins.postValue(false to plugins.filterTvTypes().filterLang().sortByQuery(currentQuery))
|
||||
}
|
||||
|
||||
fun updatePluginList(repositoryUrl: String) = viewModelScope.launchSafe {
|
||||
fun updatePluginList(context: Context?, repositoryUrl: String) = viewModelScope.launchSafe {
|
||||
if (context == null) return@launchSafe
|
||||
Log.i(TAG, "updatePluginList = $repositoryUrl")
|
||||
updatePluginListPrivate(repositoryUrl)
|
||||
updatePluginListPrivate(context, repositoryUrl)
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
|
|
|
@ -135,7 +135,8 @@ class SubtitlesFragment : Fragment() {
|
|||
it.mkdir()
|
||||
}
|
||||
return fontDir.list()?.mapNotNull {
|
||||
if (it.endsWith(".ttf")) {
|
||||
// No idea which formats are supported, but these should be.
|
||||
if (it.endsWith(".ttf") || it.endsWith(".otf")) {
|
||||
File(fontDir.absolutePath + "/" + it)
|
||||
} else null
|
||||
} ?: listOf()
|
||||
|
|
|
@ -31,6 +31,7 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.toSpanned
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -415,7 +416,7 @@ object AppUtils {
|
|||
}
|
||||
}
|
||||
|
||||
fun AppCompatActivity.loadResult(
|
||||
fun FragmentActivity.loadResult(
|
||||
url: String,
|
||||
apiName: String,
|
||||
startAction: Int = 0,
|
||||
|
|
|
@ -344,6 +344,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
VidSrcExtractor(),
|
||||
VidSrcExtractor2(),
|
||||
PlayLtXyz(),
|
||||
AStreamHub(),
|
||||
)
|
||||
|
||||
|
||||
|
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.6 MiB |
|
@ -420,14 +420,14 @@
|
|||
|
||||
|
||||
<FrameLayout
|
||||
android:nextFocusRight="@id/result_bookmark_button"
|
||||
android:id="@+id/result_movie_progress_downloaded_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="250dp">
|
||||
android:minWidth="250dp"
|
||||
android:nextFocusRight="@id/result_bookmark_button">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/result_download_movie"
|
||||
|
@ -510,17 +510,17 @@
|
|||
</FrameLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:nextFocusLeft="@id/result_movie_progress_downloaded_holder"
|
||||
android:nextFocusDown="@id/result_resume_series_button_play"
|
||||
|
||||
android:id="@+id/result_bookmark_button"
|
||||
style="@style/BlackButton"
|
||||
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="250dp"
|
||||
android:nextFocusLeft="@id/result_movie_progress_downloaded_holder"
|
||||
android:nextFocusDown="@id/result_resume_series_button_play"
|
||||
android:text="@string/type_none"
|
||||
android:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
@ -753,6 +753,16 @@
|
|||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/result_episode" />
|
||||
|
||||
<View
|
||||
android:id="@+id/temporary_no_focus"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="1dp"
|
||||
android:focusable="false"
|
||||
android:nextFocusLeft="@id/temporary_no_focus"
|
||||
android:nextFocusRight="@id/temporary_no_focus"
|
||||
android:nextFocusUp="@id/temporary_no_focus"
|
||||
android:nextFocusDown="@id/temporary_no_focus" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
@ -52,6 +52,11 @@
|
|||
|
||||
android:id="@+id/home_select_none"
|
||||
style="@style/RoundedSelectableButtonIcon"/>-->
|
||||
|
||||
<!--
|
||||
If you reorder this fix getPairList() too!
|
||||
That shit is responsible for focus selection
|
||||
-->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_select_movies"
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
<TextView
|
||||
android:id="@+id/settings_extensions"
|
||||
style="@style/SettingsItem"
|
||||
android:nextFocusUp="@id/settings_updates"
|
||||
android:nextFocusUp="@id/settings_credits"
|
||||
android:text="@string/extensions" />
|
||||
|
||||
<LinearLayout
|
||||
|
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.1 KiB |
|
@ -274,7 +274,7 @@
|
|||
<string name="episode_action_chromecast_episode">حلقة كروم كاست</string>
|
||||
<string name="episode_action_chromecast_mirror">مرآة كروم كاست</string>
|
||||
<string name="episode_action_play_in_app">تشغيل في التطبيق</string>
|
||||
<string name="episode_action_play_in_vlc">VLC تشغيل في</string>
|
||||
<string name="episode_action_play_in_format">%s تشغيل في</string>
|
||||
<string name="episode_action_play_in_browser">تشغيل في الويب </string>
|
||||
<string name="episode_action_copy_link">نسخ الرابط</string>
|
||||
<string name="episode_action_auto_download">التحميل التلقائي</string>
|
||||
|
|
|
@ -279,7 +279,7 @@
|
|||
<string name="episode_action_chromecast_episode">Episódio pelo Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Alternativa pelo Chromecast</string>
|
||||
<string name="episode_action_play_in_app">Assistir no App</string>
|
||||
<string name="episode_action_play_in_vlc">Assistir no VLC</string>
|
||||
<string name="episode_action_play_in_format">Assistir no %s</string>
|
||||
<string name="episode_action_play_in_browser">Assistir no navegador</string>
|
||||
<string name="episode_action_copy_link">Copiar link</string>
|
||||
<string name="episode_action_auto_download">Auto download</string>
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecastovat epizodu</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast jako zrcadlo</string>
|
||||
<string name="episode_action_play_in_app">Přehrát v aplikace</string>
|
||||
<string name="episode_action_play_in_vlc">Přehrát ve VLC</string>
|
||||
<string name="episode_action_play_in_format">Přehrát ve %s</string>
|
||||
<string name="episode_action_play_in_browser">Přehrát v prohlížeči</string>
|
||||
<string name="episode_action_copy_link">Zkopírovat odkaz</string>
|
||||
<string name="episode_action_auto_download">Automaticky stáhnout</string>
|
||||
|
|
|
@ -281,7 +281,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecast-Episode</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecastmirror</string>
|
||||
<string name="episode_action_play_in_app">In App wiedergeben</string>
|
||||
<string name="episode_action_play_in_vlc">In VLC wiedergeben</string>
|
||||
<string name="episode_action_play_in_format">In %s wiedergeben</string>
|
||||
<string name="episode_action_play_in_browser">In Browser wiedergeben</string>
|
||||
<string name="episode_action_copy_link">Link kopieren</string>
|
||||
<string name="episode_action_auto_download">Auto Download</string>
|
||||
|
|
|
@ -14,19 +14,6 @@
|
|||
<item>@id/cast_button_type_forward_30_seconds</item>
|
||||
</array>
|
||||
|
||||
<array name="media_type_pref">
|
||||
<item>Todos</item>
|
||||
<item>Películas y TV</item>
|
||||
<item>Anime</item>
|
||||
<item>Documental</item>
|
||||
</array>
|
||||
<array name="media_type_pref_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
</array>
|
||||
|
||||
<array name="limit_title_rez_pref_names">
|
||||
<item>@string/resolution_and_title</item>
|
||||
<item>@string/title</item>
|
||||
|
@ -169,7 +156,7 @@
|
|||
<item>@string/episode_action_chromecast_episode</item>
|
||||
<item>@string/episode_action_chromecast_mirror</item>
|
||||
<item>@string/episode_action_play_in_app</item>
|
||||
<item>@string/episode_action_play_in_vlc</item>
|
||||
<item>@string/episode_action_play_in_format</item>
|
||||
<item>@string/episode_action_play_in_browser</item>
|
||||
<item>@string/episode_action_copy_link</item>
|
||||
<item>@string/episode_action_auto_download</item>
|
||||
|
@ -222,6 +209,8 @@
|
|||
<item>Banana</item>
|
||||
<item>Fiesta</item>
|
||||
<item>Dolor rosa</item>
|
||||
<item>Material You</item>
|
||||
<item>Material You (Secondary)</item>
|
||||
</string-array>
|
||||
<string-array name="themes_overlay_names_values">
|
||||
<item>Normal</item>
|
||||
|
@ -240,19 +229,24 @@
|
|||
<item>Banana</item>
|
||||
<item>Party</item>
|
||||
<item>Pink</item>
|
||||
<item>Monet</item>
|
||||
<item>Monet2</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
<string-array name="themes_names">
|
||||
<item>Oscuro</item>
|
||||
<item>Gris</item>
|
||||
<item>Amoled</item>
|
||||
<item>Destello</item>
|
||||
<item>Material You</item>
|
||||
</string-array>
|
||||
<string-array name="themes_names_values">
|
||||
<item>AmoledLight</item>
|
||||
<item>Black</item>
|
||||
<item>Amoled</item>
|
||||
<item>Light</item>
|
||||
<item>Monet</item>
|
||||
</string-array>
|
||||
|
||||
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->
|
||||
|
|
|
@ -269,7 +269,7 @@
|
|||
<string name="episode_action_chromecast_episode">Episodio Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Espejo Chromecast</string>
|
||||
<string name="episode_action_play_in_app">Reproducir en la app</string>
|
||||
<string name="episode_action_play_in_vlc">Reproducir en VLC</string>
|
||||
<string name="episode_action_play_in_format">Reproducir en %s</string>
|
||||
<string name="episode_action_play_in_browser">Reproducir en el navegador</string>
|
||||
<string name="episode_action_copy_link">Copiar enlace</string>
|
||||
<string name="episode_action_auto_download">Descarga automática</string>
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
<string name="episode_action_chromecast_episode">Episode Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Miroir Chromecast</string>
|
||||
<string name="episode_action_play_in_app">Lecture dans l\'application</string>
|
||||
<string name="episode_action_play_in_vlc">Lecture dans VLC</string>
|
||||
<string name="episode_action_play_in_format">Lecture dans %s</string>
|
||||
<string name="episode_action_play_in_browser">Lecture dans le navigateur</string>
|
||||
<string name="episode_action_copy_link">Copier le lien</string>
|
||||
<string name="episode_action_auto_download">Téléchargement Automatique</string>
|
||||
|
|
|
@ -136,7 +136,7 @@
|
|||
<string name="episode_action_chromecast_episode">क्रोमकास्ट एपिसोड</string>
|
||||
<string name="episode_action_chromecast_mirror">कक्रोमकास्ट मिरर</string>
|
||||
<string name="episode_action_play_in_app">एप्प मैं चलाये</string>
|
||||
<string name="episode_action_play_in_vlc">VLC में चलाए</string>
|
||||
<string name="episode_action_play_in_format">%s में चलाए</string>
|
||||
<string name="episode_action_play_in_browser">Browser में चलाए</string>
|
||||
<string name="episode_action_copy_link">लिंक कॉपी करें</string>
|
||||
<string name="episode_action_auto_download">डाउनलोड करे</string>
|
||||
|
|
|
@ -299,7 +299,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecast epizoda</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
|
||||
<string name="episode_action_play_in_app">Pokreni u aplikaciji</string>
|
||||
<string name="episode_action_play_in_vlc">Pokreni u VLC-u</string>
|
||||
<string name="episode_action_play_in_format">Pokreni u %s</string>
|
||||
<string name="episode_action_play_in_browser">Pokreni u pregledniku</string>
|
||||
<string name="episode_action_copy_link">Kopiraj poveznicu</string>
|
||||
<string name="episode_action_auto_download">Automatsko preuzimanje</string>
|
||||
|
|
|
@ -264,7 +264,7 @@
|
|||
<string name="episode_action_chromecast_episode">Episode Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Mirror Chromecast</string>
|
||||
<string name="episode_action_play_in_app">Putar di aplikasi</string>
|
||||
<string name="episode_action_play_in_vlc">Putar di VLC</string>
|
||||
<string name="episode_action_play_in_format">Putar di %s</string>
|
||||
<string name="episode_action_play_in_browser">Putar di browser</string>
|
||||
<string name="episode_action_copy_link">Salin tautan</string>
|
||||
<string name="episode_action_auto_download">Download otomatis</string>
|
||||
|
|
|
@ -271,7 +271,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
|
||||
<string name="episode_action_play_in_app">Riproduci in app</string>
|
||||
<string name="episode_action_play_in_vlc">Riproduci in VLC</string>
|
||||
<string name="episode_action_play_in_format">Riproduci in %s</string>
|
||||
<string name="episode_action_play_in_browser">Riproduci nel browser</string>
|
||||
<string name="episode_action_copy_link">Copia link</string>
|
||||
<string name="episode_action_auto_download">Download</string>
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
<string name="episode_action_chromecast_episode">Епизода на Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Огледало на Chromecastr</string>
|
||||
<string name="episode_action_play_in_app">Пушти во апликацијата</string>
|
||||
<string name="episode_action_play_in_vlc">Пушти на VLC</string>
|
||||
<string name="episode_action_play_in_format">Пушти на %s</string>
|
||||
<string name="episode_action_play_in_browser">Пушти на прелистувач</string>
|
||||
<string name="episode_action_copy_link">Копирај линк</string>
|
||||
<string name="episode_action_auto_download">Авто превземање</string>
|
||||
|
|
|
@ -175,7 +175,7 @@
|
|||
<!-- <string name="episode_action_chomecast_episode">Chromecast Episode</string>
|
||||
<string name="episode_action_chomecast_mirror">Chromecast Mirror</string> -->
|
||||
<string name="episode_action_play_in_app">ആപ്പിൽ പ്ലേയ് ചെയ്യുക</string>
|
||||
<string name="episode_action_play_in_vlc">VLCയിൽ പ്ലേയ് ചെയ്യുക</string>
|
||||
<string name="episode_action_play_in_format">%sയിൽ പ്ലേയ് ചെയ്യുക</string>
|
||||
<string name="episode_action_play_in_browser">ബ്രൗസറിൽ പ്ലേയ് ചെയ്യുക</string>
|
||||
<string name="episode_action_copy_link">ലിങ്ക് പകർത്തുക</string>
|
||||
<string name="episode_action_auto_download">ഡൌൺലോഡ് ചെയ്യൂ</string>
|
||||
|
|
|
@ -145,7 +145,7 @@
|
|||
<string name="episode_action_chromecast_episode">aauugghhooo-ahah ohaaauugghh</string>
|
||||
<string name="episode_action_chromecast_mirror">aoohaaahhu ahouuhhh</string>
|
||||
<string name="episode_action_play_in_app">ooo-ahahaauuh aaahhu</string>
|
||||
<string name="episode_action_play_in_vlc">ooo-ahah ohaauuh</string>
|
||||
<string name="episode_action_play_in_format">ooo-ahah ohaauuh</string>
|
||||
<string name="episode_action_play_in_browser">ahoha ooo-ahahohoohah oooohh</string>
|
||||
<string name="episode_action_copy_link">aauugghhahhaauugghh</string>
|
||||
<string name="episode_action_auto_download">aaaghhoooohh aaahhu ahooo</string>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">Snelheid (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">Beoordeeld: %.Als</string>
|
||||
<string name="rated_format" formatted="true">Beoordeeld: %.1fAls</string>
|
||||
<string name="new_update_format" formatted="true">Nieuwe update gevonden!\n%s -> %s</string>
|
||||
<string name="filler" formatted="true">Filler</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
|
@ -274,7 +274,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecast aflevering</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
|
||||
<string name="episode_action_play_in_app">Speel in app</string>
|
||||
<string name="episode_action_play_in_vlc">Speel in VLC</string>
|
||||
<string name="episode_action_play_in_format">Speel in %s</string>
|
||||
<string name="episode_action_play_in_browser">Speel in browser</string>
|
||||
<string name="episode_action_copy_link">Kopieer link</string>
|
||||
<string name="episode_action_auto_download">Automatisch downloaden</string>
|
||||
|
|
|
@ -196,7 +196,7 @@
|
|||
<string name="episode_action_chromecast_episode">Støpt Episode</string>
|
||||
<string name="episode_action_chromecast_mirror">Støpt Speil</string>
|
||||
<string name="episode_action_play_in_app">Spill i appen</string>
|
||||
<string name="episode_action_play_in_vlc">Spill i VLC</string>
|
||||
<string name="episode_action_play_in_format">Spill i %s</string>
|
||||
<string name="episode_action_play_in_browser">Spill i nettleseren</string>
|
||||
<string name="episode_action_copy_link">Kopier link</string>
|
||||
<string name="episode_action_auto_download">Automatisk nedlasting</string>
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
<item>@string/episode_action_chromecast_episode</item>
|
||||
<item>@string/episode_action_chromecast_mirror</item>
|
||||
<item>@string/episode_action_play_in_app</item>
|
||||
<item>@string/episode_action_play_in_vlc</item>
|
||||
<item>@string/episode_action_play_in_format</item>
|
||||
<item>@string/episode_action_play_in_browser</item>
|
||||
<item>@string/episode_action_copy_link</item>
|
||||
<item>@string/episode_action_auto_download</item>
|
||||
|
@ -218,6 +218,8 @@
|
|||
<item>Bananowy</item>
|
||||
<item>Łososiowy</item>
|
||||
<item>Świnko peppowy</item>
|
||||
<item>Material You</item>
|
||||
<item>Material You (drugorzędny)</item>
|
||||
</string-array>
|
||||
<string-array name="themes_overlay_names_values">
|
||||
<item>Normal</item>
|
||||
|
@ -236,19 +238,24 @@
|
|||
<item>Banana</item>
|
||||
<item>Party</item>
|
||||
<item>Pink</item>
|
||||
<item>Monet</item>
|
||||
<item>Monet2</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
<string-array name="themes_names">
|
||||
<item>Ciemny</item>
|
||||
<item>Szary</item>
|
||||
<item>Amoled</item>
|
||||
<item>Flashbang</item>
|
||||
<item>Material You</item>
|
||||
</string-array>
|
||||
<string-array name="themes_names_values">
|
||||
<item>AmoledLight</item>
|
||||
<item>Black</item>
|
||||
<item>Amoled</item>
|
||||
<item>Light</item>
|
||||
<item>Monet</item>
|
||||
</string-array>
|
||||
|
||||
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->
|
||||
|
|
|
@ -252,7 +252,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecast odcinka</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast mirroru</string>
|
||||
<string name="episode_action_play_in_app">Odtwórz w aplikacji</string>
|
||||
<string name="episode_action_play_in_vlc">Odtwórz w VLC</string>
|
||||
<string name="episode_action_play_in_format">Odtwórz w %s</string>
|
||||
<string name="episode_action_play_in_browser">Odtwórz w przeglądarce</string>
|
||||
<string name="episode_action_copy_link">Kopiuj link</string>
|
||||
<string name="episode_action_auto_download">Automatyczne pobieranie</string>
|
||||
|
@ -455,4 +455,5 @@
|
|||
<string name="extension_types">Wspierane</string>
|
||||
<string name="extension_language">Język</string>
|
||||
<string name="extension_install_first">Najpierw zainstaluj rozszerzenie</string>
|
||||
<string name="plugins_updated">Zaaktualizowano %d rozszerzeń</string>
|
||||
</resources>
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
<string name="episode_action_chromecast_episode">Episódio pelo Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Alternativa pelo Chromecast</string>
|
||||
<string name="episode_action_play_in_app">Reproduzir na app</string>
|
||||
<string name="episode_action_play_in_vlc">Reproduzir no VLC</string>
|
||||
<string name="episode_action_play_in_format">Reproduzir no %s</string>
|
||||
<string name="episode_action_play_in_browser">Reproduzir no navegador</string>
|
||||
<string name="episode_action_copy_link">Copiar link</string>
|
||||
<string name="episode_action_auto_download">Transferência Automática</string>
|
||||
|
|
|
@ -267,7 +267,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast alternativ</string>
|
||||
<string name="episode_action_play_in_app">Redă în Aplicație</string>
|
||||
<string name="episode_action_play_in_vlc">Redă în VLC</string>
|
||||
<string name="episode_action_play_in_format">Redă în %s</string>
|
||||
<string name="episode_action_play_in_browser">Redă în Browser</string>
|
||||
<string name="episode_action_copy_link">Copiază link-ul</string>
|
||||
<string name="episode_action_auto_download">Auto-descărcare</string>
|
||||
|
|
|
@ -167,7 +167,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecasta ett Avsnitt</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecasta en Länk</string>
|
||||
<string name="episode_action_play_in_app">Spela upp i appen</string>
|
||||
<string name="episode_action_play_in_vlc">Spela upp i VLC</string>
|
||||
<string name="episode_action_play_in_format">Spela upp i %s</string>
|
||||
<string name="episode_action_play_in_browser">Spela upp i webbläsaren</string>
|
||||
<string name="episode_action_copy_link">Kopiera länk</string>
|
||||
<string name="episode_action_auto_download">Automatisk nerladdning</string>
|
||||
|
|
|
@ -204,7 +204,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecast Episode</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast Mirror</string>
|
||||
<string name="episode_action_play_in_app">I-play sa App</string>
|
||||
<string name="episode_action_play_in_vlc">I-play sa VLC</string>
|
||||
<string name="episode_action_play_in_format">I-play sa %s</string>
|
||||
<string name="episode_action_play_in_browser">I-play sa browser</string>
|
||||
<string name="episode_action_copy_link">Kopyahin ang Link</string>
|
||||
<string name="episode_action_auto_download">Awtomatiking i-download</string>
|
||||
|
|
|
@ -14,19 +14,6 @@
|
|||
<item>@id/cast_button_type_forward_30_seconds</item>
|
||||
</array>
|
||||
|
||||
<array name="media_type_pref">
|
||||
<item>Hepsi</item>
|
||||
<item>Film ve Dizi</item>
|
||||
<item>Anime</item>
|
||||
<item>Belgesel</item>
|
||||
</array>
|
||||
<array name="media_type_pref_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
</array>
|
||||
|
||||
<array name="limit_title_rez_pref_names">
|
||||
<item>@string/resolution_and_title</item>
|
||||
<item>@string/title</item>
|
||||
|
@ -169,7 +156,7 @@
|
|||
<item>@string/episode_action_chromecast_episode</item>
|
||||
<item>@string/episode_action_chromecast_mirror</item>
|
||||
<item>@string/episode_action_play_in_app</item>
|
||||
<item>@string/episode_action_play_in_vlc</item>
|
||||
<item>@string/episode_action_play_in_format</item>
|
||||
<item>@string/episode_action_play_in_browser</item>
|
||||
<item>@string/episode_action_copy_link</item>
|
||||
<item>@string/episode_action_auto_download</item>
|
||||
|
@ -222,6 +209,8 @@
|
|||
<item>Muz</item>
|
||||
<item>Parti</item>
|
||||
<item>Pembe</item>
|
||||
<item>Material You</item>
|
||||
<item>Material You (Secondary)</item>
|
||||
</string-array>
|
||||
<string-array name="themes_overlay_names_values">
|
||||
<item>Normal</item>
|
||||
|
@ -240,19 +229,24 @@
|
|||
<item>Banana</item>
|
||||
<item>Party</item>
|
||||
<item>Pink</item>
|
||||
<item>Monet</item>
|
||||
<item>Monet2</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
<string-array name="themes_names">
|
||||
<item>Koyu</item>
|
||||
<item>Gri</item>
|
||||
<item>Amoled</item>
|
||||
<item>Flaş Bombası</item>
|
||||
<item>Material You</item>
|
||||
</string-array>
|
||||
<string-array name="themes_names_values">
|
||||
<item>AmoledLight</item>
|
||||
<item>Black</item>
|
||||
<item>Amoled</item>
|
||||
<item>Light</item>
|
||||
<item>Monet</item>
|
||||
</string-array>
|
||||
|
||||
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->
|
||||
|
|
|
@ -272,7 +272,7 @@
|
|||
<string name="episode_action_chromecast_episode">Bölümü Chromecast ile yayınla</string>
|
||||
<string name="episode_action_chromecast_mirror">Bağlantıyı Chromecast ile yayınla</string>
|
||||
<string name="episode_action_play_in_app">Uygulamada oynat</string>
|
||||
<string name="episode_action_play_in_vlc">VLC\'de oynat</string>
|
||||
<string name="episode_action_play_in_format">%s\'de oynat</string>
|
||||
<string name="episode_action_play_in_browser">Tarayıcıda oynat</string>
|
||||
<string name="episode_action_copy_link">Linki kopyala</string>
|
||||
<string name="episode_action_auto_download">Otomatik indir</string>
|
||||
|
|
|
@ -14,18 +14,6 @@
|
|||
<item>@id/cast_button_type_forward_30_seconds</item>
|
||||
</array>
|
||||
|
||||
<array name="media_type_pref">
|
||||
<item>Tất cả</item>
|
||||
<item>Phim lẻ và Phim bộ</item>
|
||||
<item>Anime</item>
|
||||
<item>Phim tài liệu</item>
|
||||
</array>
|
||||
<array name="media_type_pref_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
</array>
|
||||
|
||||
<array name="limit_title_rez_pref_names">
|
||||
<item>@string/resolution_and_title</item>
|
||||
|
@ -169,7 +157,7 @@
|
|||
<item>@string/episode_action_chromecast_episode</item>
|
||||
<item>@string/episode_action_chromecast_mirror</item>
|
||||
<item>@string/episode_action_play_in_app</item>
|
||||
<item>@string/episode_action_play_in_vlc</item>
|
||||
<item>@string/episode_action_play_in_format</item>
|
||||
<item>@string/episode_action_play_in_browser</item>
|
||||
<item>@string/episode_action_copy_link</item>
|
||||
<item>@string/episode_action_auto_download</item>
|
||||
|
@ -222,6 +210,8 @@
|
|||
<item>Vàng</item>
|
||||
<item>Hồng</item>
|
||||
<item>Hồng đậm</item>
|
||||
<item>Material You</item>
|
||||
<item>Material You (Secondary)</item>
|
||||
</string-array>
|
||||
<string-array name="themes_overlay_names_values">
|
||||
<item>Normal</item>
|
||||
|
@ -240,19 +230,24 @@
|
|||
<item>Banana</item>
|
||||
<item>Party</item>
|
||||
<item>Pink</item>
|
||||
<item>Monet</item>
|
||||
<item>Monet2</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
<string-array name="themes_names">
|
||||
<item>Tối</item>
|
||||
<item>Xám</item>
|
||||
<item>Amoled</item>
|
||||
<item>Sáng</item>
|
||||
<item>Material You</item>
|
||||
</string-array>
|
||||
<string-array name="themes_names_values">
|
||||
<item>AmoledLight</item>
|
||||
<item>Black</item>
|
||||
<item>Amoled</item>
|
||||
<item>Light</item>
|
||||
<item>Monet</item>
|
||||
</string-array>
|
||||
|
||||
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->
|
||||
|
|
|
@ -292,7 +292,7 @@
|
|||
<string name="episode_action_chromecast_episode">Tập Chromecast</string>
|
||||
<string name="episode_action_chromecast_mirror">Chiếu Chromecast</string>
|
||||
<string name="episode_action_play_in_app">Xem với trình phát mặc định</string>
|
||||
<string name="episode_action_play_in_vlc">Xem với trình phát VLC</string>
|
||||
<string name="episode_action_play_in_format">Xem với trình phát %s</string>
|
||||
<string name="episode_action_play_in_browser">Xem tại trình duyệt</string>
|
||||
<string name="episode_action_copy_link">Sao chép liên kết</string>
|
||||
<string name="episode_action_auto_download">Tự động tải xuống</string>
|
||||
|
|
|
@ -303,7 +303,7 @@
|
|||
<string name="episode_action_chromecast_episode">投屏剧集</string>
|
||||
<string name="episode_action_chromecast_mirror">投屏镜像</string>
|
||||
<string name="episode_action_play_in_app">在应用中播放</string>
|
||||
<string name="episode_action_play_in_vlc">在 VLC 中播放</string>
|
||||
<string name="episode_action_play_in_format">在 %s 中播放</string>
|
||||
<string name="episode_action_play_in_browser">在浏览器中播放</string>
|
||||
<string name="episode_action_copy_link">复制链接</string>
|
||||
<string name="episode_action_auto_download">自动下载</string>
|
||||
|
|
|
@ -33,6 +33,22 @@
|
|||
<item>6</item>
|
||||
</array>
|
||||
|
||||
<array name="player_pref_names">
|
||||
<item>@string/player_settings_play_in_app</item>
|
||||
<item>@string/player_settings_play_in_vlc</item>
|
||||
<item>@string/player_settings_play_in_mpv</item>
|
||||
<item>@string/player_settings_play_in_web</item>
|
||||
<item>@string/player_settings_play_in_browser</item>
|
||||
</array>
|
||||
|
||||
<array name="player_pref_values">
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>5</item>
|
||||
<item>4</item>
|
||||
<item>3</item>
|
||||
</array>
|
||||
|
||||
<array name="limit_title_rez_pref_names">
|
||||
<item>@string/resolution_and_title</item>
|
||||
<item>@string/title</item>
|
||||
|
@ -175,7 +191,7 @@
|
|||
<item>@string/episode_action_chromecast_episode</item>
|
||||
<item>@string/episode_action_chromecast_mirror</item>
|
||||
<item>@string/episode_action_play_in_app</item>
|
||||
<item>@string/episode_action_play_in_vlc</item>
|
||||
<item>@string/episode_action_play_in_format</item>
|
||||
<item>@string/episode_action_play_in_browser</item>
|
||||
<item>@string/episode_action_copy_link</item>
|
||||
<item>@string/episode_action_auto_download</item>
|
||||
|
@ -228,6 +244,8 @@
|
|||
<item>Banana</item>
|
||||
<item>Party</item>
|
||||
<item>Pink Pain</item>
|
||||
<item>Material You</item>
|
||||
<item>Material You (Secondary)</item>
|
||||
</string-array>
|
||||
<string-array name="themes_overlay_names_values">
|
||||
<item>Normal</item>
|
||||
|
@ -246,6 +264,8 @@
|
|||
<item>Banana</item>
|
||||
<item>Party</item>
|
||||
<item>Pink</item>
|
||||
<item>Monet</item>
|
||||
<item>Monet2</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="themes_names">
|
||||
|
@ -253,12 +273,14 @@
|
|||
<item>Gray</item>
|
||||
<item>Amoled</item>
|
||||
<item>Flashbang</item>
|
||||
<item>Material You</item>
|
||||
</string-array>
|
||||
<string-array name="themes_names_values">
|
||||
<item>AmoledLight</item>
|
||||
<item>Black</item>
|
||||
<item>Amoled</item>
|
||||
<item>Light</item>
|
||||
<item>Monet</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="extension_statuses">
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
<string name="subtitle_settings_key" translatable="false">subtitle_settings_key</string>
|
||||
<string name="subtitle_settings_chromecast_key" translatable="false">subtitle_settings_chromecast_key</string>
|
||||
<string name="quality_pref_key" translatable="false">quality_pref_key</string>
|
||||
<string name="player_pref_key" translatable="false">player_pref_key</string>
|
||||
<string name="prefer_limit_title_key" translatable="false">prefer_limit_title_key</string>
|
||||
<string name="prefer_limit_title_rez_key" translatable="false">prefer_limit_title_rez_key</string>
|
||||
<string name="video_buffer_size_key" translatable="false">video_buffer_size_key</string>
|
||||
|
@ -256,7 +257,7 @@
|
|||
<string name="search">Search</string>
|
||||
<string name="category_account">Accounts</string>
|
||||
<string name="category_updates">Updates and backup</string>
|
||||
|
||||
|
||||
<string name="settings_info">Info</string>
|
||||
<string name="advanced_search">Advanced Search</string>
|
||||
<string name="advanced_search_des">Gives you the search results separated by provider</string>
|
||||
|
@ -364,7 +365,7 @@
|
|||
<string name="episode_action_chromecast_episode">Chromecast episode</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
|
||||
<string name="episode_action_play_in_app">Play in app</string>
|
||||
<string name="episode_action_play_in_vlc">Play in VLC</string>
|
||||
<string name="episode_action_play_in_format">Play in %s</string>
|
||||
<string name="episode_action_play_in_browser">Play in browser</string>
|
||||
<string name="episode_action_copy_link">Copy link</string>
|
||||
<string name="episode_action_auto_download">Auto download</string>
|
||||
|
@ -602,6 +603,7 @@
|
|||
<string name="plugins_downloaded" formatted="true">Downloaded: %d</string>
|
||||
<string name="plugins_disabled" formatted="true">Disabled: %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">Not downloaded: %d</string>
|
||||
<string name="plugins_updated" formatted="true">Updated %d plugins</string>
|
||||
<string name="blank_repo_message">Add a repository to install site extensions</string>
|
||||
<string name="view_public_repositories_button">View community repositories</string>
|
||||
<string name="view_public_repositories_button_short">Public list</string>
|
||||
|
@ -627,6 +629,14 @@
|
|||
<string name="extension_types">Supported</string>
|
||||
<string name="extension_language">Language</string>
|
||||
<string name="extension_install_first">Install the extension first</string>
|
||||
|
||||
|
||||
<string name="hls_playlist">HLS Playlist</string>
|
||||
|
||||
<string name="player_pref">Preferred video player</string>
|
||||
<string name="player_settings_play_in_app">Internal player</string>
|
||||
<string name="player_settings_play_in_vlc">VLC</string>
|
||||
<string name="player_settings_play_in_mpv">MPV</string>
|
||||
<string name="player_settings_play_in_web">Web Video Cast</string>
|
||||
<string name="player_settings_play_in_browser">Browser</string>
|
||||
<string name="app_not_found_error">App not found</string>
|
||||
</resources>
|
||||
|
|
|
@ -95,6 +95,16 @@
|
|||
<item name="white">#000</item>
|
||||
</style>
|
||||
|
||||
<style name="MonetMode">
|
||||
<item name="primaryGrayBackground">@color/material_dynamic_neutral20</item>
|
||||
<item name="primaryBlackBackground">@color/material_dynamic_neutral10</item>
|
||||
<item name="iconGrayBackground">@color/material_dynamic_neutral20</item>
|
||||
<item name="boxItemBackground">@color/material_dynamic_neutral20</item>
|
||||
<item name="textColor">@color/material_dynamic_neutral90</item>
|
||||
<item name="grayTextColor">@color/material_dynamic_neutral60</item>
|
||||
<item name="white">@color/material_dynamic_neutral90</item>
|
||||
</style>
|
||||
|
||||
<style name="OverlayPrimaryColorNormal">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:colorPrimary">@color/colorPrimary</item>
|
||||
|
@ -105,6 +115,26 @@
|
|||
<item name="android:colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="OverlayPrimaryColorMonet">
|
||||
<item name="colorPrimary">@color/material_dynamic_primary80</item>
|
||||
<item name="android:colorPrimary">@color/material_dynamic_primary80</item>
|
||||
<item name="colorPrimaryDark">@color/material_dynamic_primary30</item>
|
||||
<item name="colorAccent">@color/material_dynamic_primary80</item>
|
||||
<item name="colorOnPrimary">@color/material_dynamic_primary20</item>
|
||||
<!-- Needed for leanback fuckery -->
|
||||
<item name="android:colorAccent">@color/material_dynamic_primary30</item>
|
||||
</style>
|
||||
|
||||
<style name="OverlayPrimaryColorMonetTwo">
|
||||
<item name="colorPrimary">@color/material_dynamic_tertiary80</item>
|
||||
<item name="android:colorPrimary">@color/material_dynamic_tertiary80</item>
|
||||
<item name="colorPrimaryDark">@color/material_dynamic_tertiary30</item>
|
||||
<item name="colorAccent">@color/material_dynamic_tertiary80</item>
|
||||
<item name="colorOnPrimary">@color/material_dynamic_tertiary20</item>
|
||||
<!-- Needed for leanback fuckery -->
|
||||
<item name="android:colorAccent">@color/material_dynamic_tertiary30</item>
|
||||
</style>
|
||||
|
||||
<style name="OverlayPrimaryColorBlue">
|
||||
<item name="colorPrimary">@color/colorPrimaryBlue</item>
|
||||
<item name="android:colorPrimary">@color/colorPrimaryBlue</item>
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
android:title="@string/watch_quality_pref"
|
||||
android:icon="@drawable/ic_baseline_hd_24" />
|
||||
|
||||
<Preference
|
||||
android:key="@string/player_pref_key"
|
||||
android:title="@string/player_pref"
|
||||
android:icon="@drawable/netflix_play" />
|
||||
|
||||
<Preference
|
||||
android:key="@string/prefer_limit_title_key"
|
||||
android:title="@string/limit_title"
|
||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |