Merge branch 'master' into resource-api

This commit is contained in:
Cloudburst 2022-10-08 22:51:36 +02:00 committed by GitHub
commit b458baa06f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
118 changed files with 1233 additions and 498 deletions

View file

@ -1,8 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Report provider bug - name: Request a new provider or report bug with an existing provider
url: https://github.com/recloudstream url: https://github.com/recloudstream
about: Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord. about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
- name: Discord - name: Discord
url: https://discord.gg/5Hus6fM url: https://discord.gg/5Hus6fM
about: Join our discord for faster support on smaller issues. about: Join our discord for faster support on smaller issues.

BIN
.github/downloads.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

BIN
.github/home.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Before After
Before After

BIN
.github/player.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

BIN
.github/results.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Before After
Before After

BIN
.github/search.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Before After
Before After

View file

@ -39,9 +39,8 @@ jobs:
- name: Clean old builds - name: Clean old builds
run: | run: |
shopt -s extglob
cd $GITHUB_WORKSPACE/dokka/ cd $GITHUB_WORKSPACE/dokka/
rm -rf !(.git) rm -rf "./-cloudstream"
- name: Setup JDK 11 - name: Setup JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v1

View file

@ -2,7 +2,7 @@ name: Issue automatic actions
on: on:
issues: issues:
types: [opened, edited] types: [opened]
jobs: jobs:
issue-moderator: issue-moderator:

View file

@ -43,9 +43,7 @@ jobs:
echo "::set-output name=key_pwd::$KEY_PWD" echo "::set-output name=key_pwd::$KEY_PWD"
- name: Run Gradle - name: Run Gradle
run: | run: |
./gradlew assemblePrerelease ./gradlew assemblePrerelease makeJar androidSourcesJar
./gradlew androidSourcesJar
./gradlew makeJar
env: env:
SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}

View file

@ -206,10 +206,28 @@ task androidSourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs//full sources from android.sourceSets.main.java.srcDirs//full sources
} }
// this is used by the gradlew plugin
task makeJar(type: Copy) { task makeJar(type: Copy) {
// after modifying here, you can export. Jar
from('build/intermediates/compile_app_classes_jar/debug') from('build/intermediates/compile_app_classes_jar/debug')
into('build') // output location into('build')
include('classes.jar') // the classes file of the imported rack package include('classes.jar')
dependsOn build 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")
}
}
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

View file

@ -21,6 +21,12 @@
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="false" /> 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--> <!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
<application <application
android:name=".AcraApplication" android:name=".AcraApplication"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

View file

@ -7,10 +7,12 @@ import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.auto.service.AutoService import com.google.auto.service.AutoService
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager 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.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey 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) { override fun uncaughtException(thread: Thread, error: Throwable) {
ACRA.errorReporter.handleException(error) ACRA.errorReporter.handleException(error)
try { try {
PrintStream(errorFile).use { ps -> PrintStream(errorFile).use { ps ->
ps.println(String.format("Enabled resource pack: ${ResourcePackManager.activePackId ?: "none"}")) ps.println(String.format("Enabled resource pack: ${ResourcePackManager.activePackId ?: "none"}"))
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "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) error.printStackTrace(ps)
} }
} catch (ignored: FileNotFoundException) { } } catch (ignored: FileNotFoundException) {
}
try { try {
onError.invoke() onError.invoke()
} catch (ignored: Exception) { } } catch (ignored: Exception) {
}
exitProcess(1) exitProcess(1)
} }
@ -97,7 +108,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U
class AcraApplication : Application() { class AcraApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){ Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component)) startActivity(Intent.makeRestartActivityTask(intent!!.component))
}) })
@ -185,5 +196,15 @@ class AcraApplication : Application() {
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) { fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
context?.openBrowser(url, fallbackWebview, fragment) 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()
)
}
} }
} }

View file

@ -10,16 +10,22 @@ import android.util.Log
import android.view.* import android.view.*
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.gms.cast.framework.CastSession 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.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType 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.result.UiText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv 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.Event
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
@ -34,6 +40,7 @@ object CommonActivity {
return (this as MainActivity?)?.mSessionManager?.currentCastSession return (this as MainActivity?)?.mSessionManager?.currentCastSession
} }
var canEnterPipMode: Boolean = false var canEnterPipMode: Boolean = false
var canShowPipMode: Boolean = false var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false var isInPIPMode: Boolean = false
@ -117,7 +124,7 @@ object CommonActivity {
setLocale(this, localeCode) setLocale(this, localeCode)
} }
fun init(act: Activity?) { fun init(act: ComponentActivity?) {
if (act == null) return if (act == null) return
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //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 //https://developer.android.com/guide/topics/ui/picture-in-picture
@ -129,6 +136,22 @@ object CommonActivity {
act.updateLocale() act.updateLocale()
act.updateTv() act.updateTv()
NewPipe.init(DownloaderTestImpl.getInstance()) 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() { private fun Activity.enterPIPMode() {
@ -166,6 +189,8 @@ object CommonActivity {
"Light" -> R.style.LightMode "Light" -> R.style.LightMode
"Amoled" -> R.style.AmoledMode "Amoled" -> R.style.AmoledMode
"AmoledLight" -> R.style.AmoledModeLight "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 else -> R.style.AppTheme
} }
@ -186,6 +211,10 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana "Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty "Party" -> R.style.OverlayPrimaryColorParty
"Pink" -> R.style.OverlayPrimaryColorPink "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 else -> R.style.OverlayPrimaryColorNormal
} }
act.theme.applyStyle(currentTheme, true) act.theme.applyStyle(currentTheme, true)

View file

@ -40,6 +40,7 @@ object APIHolder {
private const val defProvider = 0 private const val defProvider = 0
// ConcurrentModificationException is possible!!!
val allProviders: MutableList<MainAPI> = arrayListOf() val allProviders: MutableList<MainAPI> = arrayListOf()
fun initAll() { fun initAll() {

View file

@ -11,10 +11,12 @@ import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy 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.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.initAll import com.lagradost.cloudstream3.APIHolder.initAll
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers 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.loadThemes
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent 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.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO 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.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey 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.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos 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.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
@ -96,17 +100,65 @@ import java.nio.charset.Charset
import kotlin.reflect.KClass import kotlin.reflect.KClass
const val VLC_PACKAGE = "org.videolan.vlc" //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result" //https://wiki.videolan.org/Android_Player_Intents/
val VLC_COMPONENT: ComponentName =
ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity")
const val VLC_REQUEST_CODE = 42
const val VLC_FROM_START = -1 //https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904
const val VLC_FROM_PROGRESS = -2 //https://mpv-android.github.io/mpv-android/intent.html
const val VLC_EXTRA_POSITION_OUT = "extra_position"
const val VLC_EXTRA_DURATION_OUT = "extra_duration" // https://www.webvideocaster.com/integrations
const val VLC_LAST_ID_KEY = "vlc_last_open_id"
//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 // Short name for requests client to make it nicer to use
@ -152,6 +204,72 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val mainPluginsLoadedEvent = val mainPluginsLoadedEvent =
Event<Boolean>() // homepage api, used to speed up time to load for homepage Event<Boolean>() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event<Boolean>() 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) { 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() { override fun onDestroy() {
val broadcastIntent = Intent() val broadcastIntent = Intent()
broadcastIntent.action = "restart_service" broadcastIntent.action = "restart_service"
@ -356,56 +449,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (intent == null) return if (intent == null) return
val str = intent.dataString val str = intent.dataString
loadCache() loadCache()
if (str != null) { handleAppIntentUrl(this, str, false)
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
}
}
}
}
}
} }
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean = 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 // 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 APIHolder.apiMap = null
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@ -480,7 +525,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
val settingsForProvider = SettingsJson() 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 MainAPI.settingsForProvider = settingsForProvider
@ -514,7 +560,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
ioSafe { 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) PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else { } else {
PluginManager.loadAllOnlinePlugins(this@MainActivity) PluginManager.loadAllOnlinePlugins(this@MainActivity)
@ -558,9 +608,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
for (api in accountManagers) { for (api in accountManagers) {
api.init() api.init()
} }
}
ioSafe {
inAppAuths.apmap { api -> inAppAuths.apmap { api ->
try { try {
api.initialize() api.initialize()

View file

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

View file

@ -3,10 +3,9 @@ package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.delay
import com.lagradost.cloudstream3.utils.M3u8Helper import java.net.URI
import com.lagradost.cloudstream3.utils.loadExtractor
class VidSrcExtractor2 : VidSrcExtractor() { class VidSrcExtractor2 : VidSrcExtractor() {
override val mainUrl = "https://vidsrc.me/embed" override val mainUrl = "https://vidsrc.me/embed"
@ -27,6 +26,25 @@ open class VidSrcExtractor : ExtractorApi() {
override val mainUrl = "$absoluteUrl/embed" override val mainUrl = "$absoluteUrl/embed"
override val requiresReferer = false 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( override suspend fun getUrl(
url: String, url: String,
referer: String?, referer: String?,
@ -40,7 +58,10 @@ open class VidSrcExtractor : ExtractorApi() {
val datahash = it.attr("data-hash") val datahash = it.attr("data-hash")
if (datahash.isNotBlank()) { if (datahash.isNotBlank()) {
val links = try { 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) { } catch (e: Exception) {
"" ""
} }
@ -54,11 +75,28 @@ open class VidSrcExtractor : ExtractorApi() {
val srcresponse = app.get(server, referer = absoluteUrl).text val srcresponse = app.get(server, referer = absoluteUrl).text
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap
M3u8Helper.generateM3u8( val passRegex = Regex("""['"](.*set_pass[^"']*)""")
name, val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
Regex("""^//"""), "https://"
)
callback.invoke(
ExtractorLink(
this.name,
this.name,
srcm3u8, srcm3u8,
absoluteUrl this.mainUrl,
).forEach(callback) Qualities.Unknown.value,
extractorData = pass,
isM3u8 = true
)
)
// M3u8Helper.generateM3u8(
// name,
// srcm3u8,
// absoluteUrl
// ).forEach(callback)
} else { } else {
loadExtractor(linkfixed, url, subtitleCallback, callback) loadExtractor(linkfixed, url, subtitleCallback, callback)
} }

View file

@ -15,6 +15,7 @@ import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
const val DEBUG_EXCEPTION = "THIS IS A DEBUG EXCEPTION!" const val DEBUG_EXCEPTION = "THIS IS A DEBUG EXCEPTION!"
const val DEBUG_PRINT = "DEBUG PRINT"
class DebugException(message: String) : Exception("$DEBUG_EXCEPTION\n$message") 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) { inline fun debugWarning(message: () -> String) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
logError(DebugException(message.invoke())) logError(DebugException(message.invoke()))

View file

@ -1,13 +1,17 @@
package com.lagradost.cloudstream3.plugins package com.lagradost.cloudstream3.plugins
import android.app.*
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import com.google.gson.Gson import com.google.gson.Gson
import android.content.res.AssetManager import android.content.res.AssetManager
import android.content.res.Resources import android.content.res.Resources
import android.os.Environment import android.os.Environment
import android.widget.Toast import android.widget.Toast
import android.app.Activity import android.content.Context
import android.os.Build
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey 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.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 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.ExtractorApi
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.resources.ResourcePackManager import com.lagradost.cloudstream3.utils.resources.ResourcePackManager
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.acra.log.debug
import java.io.File import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.* import java.util.*
@ -39,6 +46,9 @@ import java.util.*
const val PLUGINS_KEY = "PLUGINS_KEY" const val PLUGINS_KEY = "PLUGINS_KEY"
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL" 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 for internal storage
data class PluginData( data class PluginData(
@ -79,6 +89,8 @@ object PluginManager {
const val TAG = "PluginManager" const val TAG = "PluginManager"
private var hasCreatedNotChanel = false
/** /**
* Store data about the plugin for fetching later * Store data about the plugin for fetching later
* */ * */
@ -113,6 +125,10 @@ object PluginManager {
val plugins = getPluginsOnline().filter { val plugins = getPluginsOnline().filter {
!it.filePath.contains(repositoryPath) !it.filePath.contains(repositoryPath)
} }
val file = File(repositoryPath)
normalSafeApiCall {
if (file.exists()) file.deleteRecursively()
}
setKey(PLUGINS_KEY, plugins) setKey(PLUGINS_KEY, plugins)
} }
} }
@ -164,8 +180,16 @@ object PluginManager {
val onlineData: Pair<String, SitePlugin>, val onlineData: Pair<String, SitePlugin>,
) { ) {
val isOutdated = 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 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() // var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
@ -211,28 +235,41 @@ object PluginManager {
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
val outdatedPlugins = getPluginsOnline().map { savedData -> val outdatedPlugins = getPluginsOnline().map { savedData ->
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName } onlinePlugins
.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
.map { onlineData -> .map { onlineData ->
OnlinePluginData(savedData, onlineData) OnlinePluginData(savedData, onlineData)
}.filter {
it.validOnlineData(activity)
} }
}.flatten().distinctBy { it.onlineData.second.url } }.flatten().distinctBy { it.onlineData.second.url }
debug { debugPrint {
"Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}" "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}"
} }
val updatedPlugins = mutableListOf<String>()
outdatedPlugins.apmap { pluginData -> outdatedPlugins.apmap { pluginData ->
if (pluginData.isDisabled) { if (pluginData.isDisabled) {
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
unloadPlugin(pluginData.savedData.filePath) unloadPlugin(pluginData.savedData.filePath)
} else if (pluginData.isOutdated) { } else if (pluginData.isOutdated) {
downloadAndLoadPlugin( downloadAndLoadPlugin(
activity, activity,
pluginData.onlineData.second.url, pluginData.onlineData.second.url,
pluginData.savedData.internalName, 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 { ioSafe {
afterPluginsLoadedEvent.invoke(true) afterPluginsLoadedEvent.invoke(true)
@ -396,24 +433,61 @@ object PluginManager {
) + "." + name.hashCode() ) + "." + 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( suspend fun downloadAndLoadPlugin(
activity: Activity, activity: Activity,
pluginUrl: String, pluginUrl: String,
internalName: String, internalName: String,
repositoryUrl: String repositoryUrl: String
): Boolean { ): Boolean {
try { val file = getPluginPath(activity, internalName, repositoryUrl)
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique downloadAndLoadPlugin(activity, pluginUrl, internalName, file)
val fileName = getPluginSanitizedFileName(internalName) return true
unloadPlugin("${activity.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3") }
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 // 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( return loadPlugin(
activity, activity,
file ?: return false, newFile ?: return false,
PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET) PluginData(
internalName,
pluginUrl,
true,
newFile.absolutePath,
PLUGIN_VERSION_NOT_SET
)
) )
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@ -421,18 +495,13 @@ object PluginManager {
} }
} }
/** suspend fun deletePlugin(file: File): Boolean {
* @param isFilePath will treat the pluginUrl as as the filepath instead of url val list = (getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
* */
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
return try { return try {
if (File(data.filePath).delete()) { if (File(file.absolutePath).delete()) {
unloadPlugin(data.filePath) unloadPlugin(file.absolutePath)
deletePluginData(data) list.forEach { deletePluginData(it) }
return true return true
} }
false false
@ -440,4 +509,63 @@ object PluginManager {
false 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
}
}
} }

View file

@ -84,7 +84,7 @@ object RepositoryManager {
// Normal parsed function not working? // Normal parsed function not working?
// return response.parsedSafe() // return response.parsedSafe()
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList() tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
} catch (t : Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
emptyList() emptyList()
} }
@ -102,9 +102,27 @@ object RepositoryManager {
}.flatten() }.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( suspend fun downloadPluginToFile(
context: Context, context: Context,
pluginUrl: String, pluginUrl: String,
/** Filename without .cs3 */
fileName: String, fileName: String,
folder: String folder: String
): File? { ): File? {

View file

@ -10,6 +10,9 @@ import java.security.MessageDigest
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main 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 object VotingApi { // please do not cheat the votes lol
private const val LOGKEY = "VotingApi" private const val LOGKEY = "VotingApi"
@ -64,7 +67,8 @@ object VotingApi { // please do not cheat the votes lol
} }
private suspend fun createBucket(pluginUrl: String) { 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") Log.d(LOGKEY, "Requesting: $url")
app.get(url) app.get(url)
} }
@ -74,33 +78,47 @@ object VotingApi { // please do not cheat the votes lol
return true return true
} }
private val voteLock = Mutex()
suspend fun vote(pluginUrl: String, requestType: VoteType): Int { suspend fun vote(pluginUrl: String, requestType: VoteType): Int {
// Prevent multiple requests at the same time.
voteLock.withLock {
if (!canVote(pluginUrl)) { if (!canVote(pluginUrl)) {
main { main {
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT).show() 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 val savedType: VoteType =
var changeValue = 0 getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
if (requestType == savedType) {
newType = VoteType.NONE val newType = if (requestType == savedType) VoteType.NONE else requestType
changeValue = -requestType.value val changeValue = if (requestType == savedType) {
-requestType.value
} else if (savedType == VoteType.NONE) { } else if (savedType == VoteType.NONE) {
changeValue = requestType.value requestType.value
} else if (savedType != requestType) { } else if (savedType != requestType) {
changeValue = -savedType.value + requestType.value -savedType.value + requestType.value
} } else 0
val url = "${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}"
// Pre-emptively set vote key
setKey("cs3-votes/${transformUrl(pluginUrl)}", newType)
val url =
"${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}"
Log.d(LOGKEY, "Requesting: $url") Log.d(LOGKEY, "Requesting: $url")
val res = app.get(url).parsedSafe<Result>()?.value val res = app.get(url).parsedSafe<Result>()?.value
if (res != null) {
setKey("cs3-votes/${transformUrl(pluginUrl)}", newType) if (res == null) {
// "Refund" key if the response is invalid
setKey("cs3-votes/${transformUrl(pluginUrl)}", savedType)
} else {
votesCache[pluginUrl] = res votesCache[pluginUrl] = res
} }
return res ?: 0 return res ?: 0
} }
}
private data class Result( private data class Result(
val value: Int? val value: Int?

View file

@ -37,7 +37,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
val subtitleProviders val subtitleProviders
get() = listOf( get() = listOf(
openSubtitlesApi, openSubtitlesApi,
// indexSubtitlesApi // they got anti scraping measures in place :( indexSubtitlesApi // they got anti scraping measures in place :(
) )
const val appString = "cloudstreamapp" const val appString = "cloudstreamapp"

View file

@ -1,9 +1,11 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import androidx.fragment.app.FragmentActivity
interface OAuth2API : AuthAPI { interface OAuth2API : AuthAPI {
val key: String val key: String
val redirectUrl: String val redirectUrl: String
suspend fun handleRedirect(url: String) : Boolean suspend fun handleRedirect(url: String) : Boolean
fun authenticate() fun authenticate(activity: FragmentActivity?)
} }

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
@ -48,9 +49,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
removeAccountKeys() removeAccountKeys()
} }
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token" 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 { override suspend fun handleRedirect(url: String): Boolean {

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.OAuth2API
@ -15,7 +16,7 @@ class Dropbox : OAuth2API {
override val icon: Int override val icon: Int
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View file

@ -20,10 +20,9 @@ class IndexSubtitleApi : AbstractSubApi {
override fun logOut() {} override fun logOut() {}
private val interceptor = CloudflareKiller()
companion object { companion object {
const val host = "https://indexsubtitle.com" const val host = "https://subscene.cyou"
const val TAG = "INDEXSUBS" const val TAG = "INDEXSUBS"
} }
@ -126,16 +125,15 @@ class IndexSubtitleApi : AbstractSubApi {
epNumber = epNum, epNumber = epNum,
seasonNumber = seasonNum, seasonNumber = seasonNum,
year = yearNum, 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 -> document.select("div.my-3.p-3 div.media").map { block ->
if (seasonNum > 0) { 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) val season = getOrdinal(seasonNum)
if ((block.selectFirst("a")?.attr("href") if ((block.selectFirst("a")?.attr("href")
?.contains( ?.contains(
@ -163,7 +161,7 @@ class IndexSubtitleApi : AbstractSubApi {
val urlItem = fixUrl( val urlItem = fixUrl(
it.selectFirst("a")!!.attr("href") it.selectFirst("a")!!.attr("href")
) )
val itemDoc = app.get(urlItem, interceptor = interceptor).document val itemDoc = app.get(urlItem).document
val id = imdbUrlToIdNullable( val id = imdbUrlToIdNullable(
itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent() itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent()
?.attr("href") ?.attr("href")
@ -202,14 +200,14 @@ class IndexSubtitleApi : AbstractSubApi {
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>() val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
urlItems.forEach { url -> urlItems.forEach { url ->
val request = app.get(url, interceptor = interceptor) val request = app.get(url)
if (request.isSuccessful) { if (request.isSuccessful) {
request.document.select("div.my-3.p-3 div.media").map { block -> request.document.select("div.my-3.p-3 div.media").map { block ->
if (block.select("span.d-block span[data-original-title=Language]").text() if (block.select("span.d-block span[data-original-title=Language]").text()
.trim() .trim()
.contains("$queryLang") .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")) val link = fixUrl(block.selectFirst("a")!!.attr("href"))
if (seasonNum > 0) { if (seasonNum > 0) {
when { when {
@ -235,7 +233,7 @@ class IndexSubtitleApi : AbstractSubApi {
val seasonNum = data.seasonNumber val seasonNum = data.seasonNumber
val epNum = data.epNumber val epNum = data.epNumber
val req = app.get(data.data, interceptor = interceptor) val req = app.get(data.data)
if (req.isSuccessful) { if (req.isSuccessful) {
val document = req.document val document = req.document
@ -246,7 +244,7 @@ class IndexSubtitleApi : AbstractSubApi {
} else { } else {
document.select("div.my-3.p-3 div.media").mapNotNull { block -> document.select("div.my-3.p-3 div.media").mapNotNull { block ->
val name = val name =
block.selectFirst("strong.d-block.text-primary")?.text()?.trim().toString() block.selectFirst("strong.d-block")?.text()?.trim().toString()
if (seasonNum!! > 0) { if (seasonNum!! > 0) {
if (isRightEps(name, seasonNum, epNum)) { if (isRightEps(name, seasonNum, epNum)) {
fixUrl(block.selectFirst("a")!!.attr("href")) fixUrl(block.selectFirst("a")!!.attr("href"))

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Base64 import android.util.Base64
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
@ -281,7 +282,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return false return false
} }
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
// It is recommended to use a URL-safe string as code_verifier. // It is recommended to use a URL-safe string as code_verifier.
// See section 4 of RFC 7636 for more details. // See section 4 of RFC 7636 for more details.
@ -294,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val codeChallenge = codeVerifier val codeChallenge = codeVerifier
val request = val request =
"$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" "$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 private var requestId = 0

View file

@ -11,12 +11,12 @@ import android.webkit.WebViewClient
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import kotlinx.android.synthetic.main.fragment_webview.* import kotlinx.android.synthetic.main.fragment_webview.*
import java.net.URI
class WebviewFragment : Fragment() { class WebviewFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -31,16 +31,8 @@ class WebviewFragment : Fragment() {
request: WebResourceRequest? request: WebResourceRequest?
): Boolean { ): Boolean {
val requestUrl = request?.url.toString() val requestUrl = request?.url.toString()
val repoUrl = if (requestUrl.startsWith("https://cs.repo")) { val performedAction = MainActivity.handleAppIntentUrl(activity, requestUrl, true)
"https://" + requestUrl.substringAfter("?") if (performedAction) {
} else if (URI(requestUrl).scheme == appStringRepo) {
requestUrl.replaceFirst(appStringRepo, "https")
} else {
null
}
if (repoUrl != null) {
activity?.loadRepository(repoUrl)
findNavController().popBackStack() findNavController().popBackStack()
return true return true
} }
@ -48,12 +40,15 @@ class WebviewFragment : Fragment() {
return super.shouldOverrideUrlLoading(view, request) 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 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) web_view.loadUrl(url)
} }

View file

@ -256,13 +256,14 @@ class HomeFragment : Fragment() {
nsfw: MaterialButton?, nsfw: MaterialButton?,
others: MaterialButton?, others: MaterialButton?,
): List<Pair<MaterialButton?, List<TvType>>> { ): List<Pair<MaterialButton?, List<TvType>>> {
// This list should be same order as home screen to aid navigation
return listOf( 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(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(asian, listOf(TvType.AsianDrama)),
Pair(cartoons, listOf(TvType.Cartoon)),
Pair(docs, listOf(TvType.Documentary)),
Pair(livestream, listOf(TvType.Live)), Pair(livestream, listOf(TvType.Live)),
Pair(nsfw, listOf(TvType.NSFW)), Pair(nsfw, listOf(TvType.NSFW)),
Pair(others, listOf(TvType.Others)), Pair(others, listOf(TvType.Others)),
@ -352,11 +353,25 @@ class HomeFragment : Fragment() {
arrayAdapter.notifyDataSetChanged() 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) { for ((button, validTypes) in pairList) {
val isValid = val isValid =
validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } } validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } }
button?.isVisible = isValid button?.isVisible = isValid
if (isValid) { if (isValid) {
// Set focus navigation
button?.let { currentButton ->
lastButton?.nextFocusRightId = currentButton.id
lastButton?.id?.let { currentButton.nextFocusLeftId = it }
lastButton = currentButton
}
fun buttonContains(): Boolean { fun buttonContains(): Boolean {
return preSelectedTypes.any { validTypes.contains(it) } return preSelectedTypes.any { validTypes.contains(it) }
} }
@ -506,16 +521,13 @@ class HomeFragment : Fragment() {
} }
} }
//Disable Random button, if its toggled off on settings //Load value for toggling Random button. Hide at startup
context?.let { context?.let {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
toggleRandomButton = toggleRandomButton =
settingsManager.getBoolean(getString(R.string.random_button_key), false) 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 -> observe(homeViewModel.apiName) { apiName ->
currentApiName = apiName currentApiName = apiName
@ -611,6 +623,7 @@ class HomeFragment : Fragment() {
home_loading_shimmer?.stopShimmer() home_loading_shimmer?.stopShimmer()
val d = data.value val d = data.value
val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear() listHomepageItems.clear()
// println("ITEMCOUNT: ${d.values.size} ${home_master_recycler?.adapter?.itemCount}") // println("ITEMCOUNT: ${d.values.size} ${home_master_recycler?.adapter?.itemCount}")
@ -623,6 +636,11 @@ class HomeFragment : Fragment() {
home_loading_error?.isVisible = false home_loading_error?.isVisible = false
home_loaded?.isVisible = true home_loaded?.isVisible = true
if (toggleRandomButton) { if (toggleRandomButton) {
//Flatten list
d.values.forEach { dlist ->
mutableListOfResponse.addAll(dlist.list.list)
}
listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url })
home_random?.isVisible = listHomepageItems.isNotEmpty() home_random?.isVisible = listHomepageItems.isNotEmpty()
} else { } else {
home_random?.isGone = true home_random?.isGone = true

View file

@ -276,9 +276,6 @@ class HomeViewModel : ViewModel() {
if (preferredApiName == noneApi.name) { if (preferredApiName == noneApi.name) {
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
loadAndCancel(noneApi) 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) { } else if (preferredApiName == randomApi.name) {
val validAPIs = context?.filterProviderByPreferredMedia() val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) { if (validAPIs.isNullOrEmpty()) {
@ -289,6 +286,9 @@ class HomeViewModel : ViewModel() {
loadAndCancel(apiRandom) loadAndCancel(apiRandom)
setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) 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 { } else {
setKey(USER_SELECTED_HOMEPAGE_API, api.name) setKey(USER_SELECTED_HOMEPAGE_API, api.name)
loadAndCancel(api) loadAndCancel(api)

View file

@ -611,6 +611,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_lock?.isGone = !isShowing player_lock?.isGone = !isShowing
//player_media_route_button?.isClickable = !isGone //player_media_route_button?.isClickable = !isGone
player_go_back_holder?.isGone = isGone player_go_back_holder?.isGone = isGone
player_sources_btt?.isGone = isGone
} }
private fun updateLockUI() { private fun updateLockUI() {

View file

@ -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_source_and_subs.subtitles_click_settings
import kotlinx.android.synthetic.main.player_select_tracks.* import kotlinx.android.synthetic.main.player_select_tracks.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
class GeneratorPlayer : FullScreenPlayer() { class GeneratorPlayer : FullScreenPlayer() {
companion object { companion object {
@ -115,10 +116,11 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onTracksInfoChanged() { override fun onTracksInfoChanged() {
val tracks = player.getVideoTracks() 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. // Only set the preferred language if it is available.
// Otherwise it may give some users audio track init failed! // 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) player.setPreferredAudioTrack(preferredAudioTrackLanguage)
} }
} }
@ -602,9 +604,21 @@ class GeneratorPlayer : FullScreenPlayer() {
subtitleList.setItemChecked(subtitleIndex, true) subtitleList.setItemChecked(subtitleIndex, true)
subtitleList.setOnItemClickListener { _, _, which, _ -> subtitleList.setOnItemClickListener { _, _, which, _ ->
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 subtitleIndex = which
subtitleList.setItemChecked(which, true) subtitleList.setItemChecked(which, true)
} }
}
sourceDialog.cancel_btt?.setOnClickListener { sourceDialog.cancel_btt?.setOnClickListener {
sourceDialog.dismissSafe(activity) sourceDialog.dismissSafe(activity)
@ -762,7 +776,8 @@ class GeneratorPlayer : FullScreenPlayer() {
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice) ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
// audioArrayAdapter.add(ctx.getString(R.string.no_subtitles)) // audioArrayAdapter.add(ctx.getString(R.string.no_subtitles))
audioArrayAdapter.addAll(currentAudioTracks.mapIndexed { index, format -> 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 audioList.adapter = audioArrayAdapter

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -9,6 +10,7 @@ import android.widget.TextView
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar import androidx.core.widget.ContentLoadingProgressBar
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton 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 = 13
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14 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) data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
class EpisodeAdapter( class EpisodeAdapter(
@ -60,7 +66,26 @@ class EpisodeAdapter(
private val clickCallback: (EpisodeClickEvent) -> Unit, private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : 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 val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? { private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
@ -239,7 +264,6 @@ class EpisodeAdapter(
itemView.setOnLongClickListener { itemView.setOnLongClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
return@setOnLongClickListener true return@setOnLongClickListener true
} }

View file

@ -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.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment 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.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.* 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_swipe.*
import kotlinx.android.synthetic.main.fragment_result_tv.* import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.result_sync.* import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -293,7 +295,7 @@ open class ResultFragment : ResultTrailerPlayer() {
result_reload_connection_open_in_browser?.isVisible = true result_reload_connection_open_in_browser?.isVisible = true
} }
2 -> { 2 -> {
result_bookmark_fab?.isGone = isTvSettings() result_bookmark_fab?.isGone = isTrueTvSettings()
result_bookmark_fab?.extend() result_bookmark_fab?.extend()
//if (result_bookmark_button?.context?.isTrueTvSettings() == true) { //if (result_bookmark_button?.context?.isTrueTvSettings() == true) {
// when { // when {
@ -412,7 +414,39 @@ open class ResultFragment : ResultTrailerPlayer() {
is ResourceSome.Success -> { is ResourceSome.Success -> {
result_episodes?.isVisible = true result_episodes?.isVisible = true
result_episode_loading?.isVisible = false 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) (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 apiName: String,
val showFillers: Boolean, val showFillers: Boolean,
val dubStatus: DubStatus, val dubStatus: DubStatus,
val start: AutoResume? val start: AutoResume?,
val playerAction: Int
) )
private fun getStoredData(context: Context): StoredData? { private fun getStoredData(context: Context): StoredData? {
@ -436,6 +471,8 @@ open class ResultFragment : ResultTrailerPlayer() {
) DubStatus.Dubbed else DubStatus.Subbed ) DubStatus.Dubbed else DubStatus.Subbed
val startAction = arguments?.getInt(START_ACTION_BUNDLE) val startAction = arguments?.getInt(START_ACTION_BUNDLE)
val playerAction = getPlayerAction(context)
val start = startAction?.let { action -> val start = startAction?.let { action ->
val startValue = arguments?.getInt(START_VALUE_BUNDLE) val startValue = arguments?.getInt(START_VALUE_BUNDLE)
val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE) val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE)
@ -450,7 +487,7 @@ open class ResultFragment : ResultTrailerPlayer() {
season = resumeSeason season = resumeSeason
) )
} }
return StoredData(url, apiName, showFillers, dubStatus, start) return StoredData(url, apiName, showFillers, dubStatus, start, playerAction)
} }
private fun reloadViewModel(success: Boolean = false) { private fun reloadViewModel(success: Boolean = false) {
@ -458,7 +495,14 @@ open class ResultFragment : ResultTrailerPlayer() {
val storedData = getStoredData(activity ?: context ?: return) ?: return val storedData = getStoredData(activity ?: context ?: return) ?: return
//viewModel.clear() //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( viewModel.handleAction(
activity, activity,
EpisodeClickEvent( 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) { if (storedData?.url != null) {
result_reload_connectionerror.setOnClickListener { 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 { result_reload_connection_open_in_browser?.setOnClickListener {
@ -952,7 +1004,14 @@ open class ResultFragment : ResultTrailerPlayer() {
if (restart || !viewModel.hasLoaded()) { if (restart || !viewModel.hasLoaded()) {
//viewModel.clear() //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
)
} }
} }
} }

View file

@ -1,14 +1,13 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.app.Activity import android.app.Activity
import android.content.ClipData import android.content.*
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -16,6 +15,7 @@ import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.getCastSession
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer 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.IGenerator
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData 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.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull 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.ioWork
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
import com.lagradost.cloudstream3.utils.Coroutines.main 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.getDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason 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.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason 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.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.lang.Math.abs import java.lang.Math.abs
@ -615,7 +613,7 @@ class ResultViewModel2 : ViewModel() {
val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let
// SET VISUAL KEYS // SET VISUAL KEYS
AcraApplication.setKey( setKey(
DOWNLOAD_HEADER_CACHE, DOWNLOAD_HEADER_CACHE,
parentId.toString(), parentId.toString(),
VideoDownloadHelper.DownloadHeaderCached( VideoDownloadHelper.DownloadHeaderCached(
@ -629,7 +627,7 @@ class ResultViewModel2 : ViewModel() {
) )
) )
AcraApplication.setKey( setKey(
DataStore.getFolderName( DataStore.getFolderName(
DOWNLOAD_EPISODE_CACHE, DOWNLOAD_EPISODE_CACHE,
parentId.toString() parentId.toString()
@ -956,71 +954,156 @@ class ResultViewModel2 : ViewModel() {
return LinkLoadingResult(sortUrls(links), sortSubs(subs)) return LinkLoadingResult(sortUrls(links), sortSubs(subs))
} }
private fun playWithVlc(act: Activity?, data: LinkLoadingResult, id: Int) = ioSafe { private fun launchActivity(
if (act == null) return@ioSafe activity: Activity?,
if (data.links.isEmpty()) { resumeApp: ResultResume,
showToast(act, R.string.no_links_found_toast, Toast.LENGTH_SHORT) id: Int? = null,
return@ioSafe work: suspend (Intent.(Activity) -> Unit)
} ): Job? {
val act = activity ?: return null
return CoroutineScope(Dispatchers.IO).launch {
try { try {
if (!act.checkWrite()) { resumeApp.launch(id) {
act.requestRW() work(act)
if (act.checkWrite()) return@ioSafe }
} 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)
}
}
}
}
} }
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 val outputDir = act.cacheDir
val outputFile = withContext(Dispatchers.IO) {
File.createTempFile("mirrorlist", ".m3u8", outputDir) if (singleFile ?: (data.links.size == 1)) {
} setDataAndType(data.links.first().url.toUri(), "video/*")
} else {
val outputFile = File.createTempFile("mirrorlist", ".m3u8", outputDir)
var text = "#EXTM3U" 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) { for (link in data.links) {
text += "\n#EXTINF:, ${link.name}\n${link.url}" text += "\n#EXTINF:, ${link.name}\n${link.url}"
} }
outputFile.writeText(text) outputFile.writeText(text)
val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT) setDataAndType(
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(
FileProvider.getUriForFile( FileProvider.getUriForFile(
act, act,
act.applicationContext.packageName + ".provider", act.applicationContext.packageName + ".provider",
outputFile outputFile
), "video/*" ), "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) val position = if (resume) {
getViewPos(id)?.position ?: 0L
vlcIntent.component = VLC_COMPONENT } else {
act.setKey(VLC_LAST_ID_KEY, id) 1L
act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE)
} catch (e: Exception) {
logError(e)
showToast(act, e.toString(), Toast.LENGTH_LONG)
}
} }
fun handleAction(activity: Activity?, click: EpisodeClickEvent) = viewModelScope.launchSafe { component = VLC_COMPONENT
putExtra("from_start", !resume)
putExtra("position", position)
}
fun handleAction(activity: Activity?, click: EpisodeClickEvent) =
viewModelScope.launchSafe {
handleEpisodeClickEvent(activity, click) 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) { private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) {
when (click.action) { when (click.action) {
ACTION_SHOW_OPTIONS -> { ACTION_SHOW_OPTIONS -> {
@ -1035,9 +1118,17 @@ class ResultViewModel2 : ViewModel() {
} }
options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER)
if (activity?.isAppInstalled(VLC_PACKAGE) == true) { for (app in apps) {
options.add(txt(R.string.episode_action_play_in_vlc) to ACTION_PLAY_EPISODE_IN_VLC_PLAYER) if (activity?.isAppInstalled(app.packageString) == true) {
options.add(
txt(
R.string.episode_action_play_in_format,
txt(app.name)
) to app.action
)
} }
}
options.addAll( options.addAll(
listOf( listOf(
txt(R.string.episode_action_play_in_browser) to ACTION_PLAY_EPISODE_IN_BROWSER, 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) click.copy(action = ACTION_CHROME_CAST_EPISODE)
) )
} else { } else {
val action = getPlayerAction(ctx)
handleEpisodeClickEvent( handleEpisodeClickEvent(
activity, 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 -> { ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
loadLinks(click.data, isVisible = true, isCasting = true) { links -> 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( playWithVlc(
activity, activity,
links, 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 -> { ACTION_PLAY_EPISODE_IN_PLAYER -> {
val data = currentResponse?.syncData?.toList() ?: emptyList() val data = currentResponse?.syncData?.toList() ?: emptyList()
val list = val list =
@ -1284,7 +1412,11 @@ class ResultViewModel2 : ViewModel() {
}, { }, {
if (this !is AnimeLoadResponse) return@argamap if (this !is AnimeLoadResponse) return@argamap
val map = val map =
Kitsu.getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false) Kitsu.getEpisodesDetails(
getMalId(),
getAniListId(),
isResponseRequired = false
)
if (map.isNullOrEmpty()) return@argamap if (map.isNullOrEmpty()) return@argamap
updateEpisodes = DubStatus.values().map { dubStatus -> updateEpisodes = DubStatus.values().map { dubStatus ->
val current = val current =
@ -1304,8 +1436,10 @@ class ResultViewModel2 : ViewModel() {
val currentBack = this val currentBack = this
this.description = this.description ?: node.description?.en this.description = this.description ?: node.description?.en
this.name = this.name ?: node.titles?.canonical this.name = this.name ?: node.titles?.canonical
this.episode = this.episode ?: node.num ?: episodeNumbers[index] this.episode =
this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url 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 val idIndex = ep.key.id
for ((index, i) in ep.value.withIndex()) { for ((index, i) in ep.value.withIndex()) {
val episode = i.episode ?: (index + 1) 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)) { if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id) existingEpisodes.add(id)
val seasonData = loadResponse.seasonNames.getSeason(i.season) val seasonData = loadResponse.seasonNames.getSeason(i.season)
@ -1888,7 +2024,10 @@ class ResultViewModel2 : ViewModel() {
if (ep.getWatchProgress() > 0.9) continue if (ep.getWatchProgress() > 0.9) continue
handleAction( handleAction(
activity, activity,
EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep) EpisodeClickEvent(
getPlayerAction(activity),
ep
)
) )
break break
} }
@ -1905,7 +2044,10 @@ class ResultViewModel2 : ViewModel() {
?: return@launchSafe ?: return@launchSafe
handleAction( handleAction(
activity, activity,
EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, episode) EpisodeClickEvent(
getPlayerAction(activity),
episode
)
) )
} }
} }
@ -1983,7 +2125,7 @@ class ResultViewModel2 : ViewModel() {
preferStartEpisode = getResultEpisode(mainId) preferStartEpisode = getResultEpisode(mainId)
preferStartSeason = getResultSeason(mainId) preferStartSeason = getResultSeason(mainId)
AcraApplication.setKey( setKey(
DOWNLOAD_HEADER_CACHE, DOWNLOAD_HEADER_CACHE,
mainId.toString(), mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached( VideoDownloadHelper.DownloadHeaderCached(

View file

@ -1,8 +1,5 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.* import android.view.View.*
@ -12,8 +9,10 @@ import androidx.annotation.UiThread
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -39,7 +38,11 @@ import kotlinx.android.synthetic.main.add_account_input.*
class SettingsAccount : PreferenceFragmentCompat() { class SettingsAccount : PreferenceFragmentCompat() {
companion object { companion object {
/** Used by nginx plugin too */ /** 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 = val builder =
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom)
.setView(R.layout.account_managment) .setView(R.layout.account_managment)
@ -62,9 +65,13 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
showAccountSwitch(activity, api) 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 accounts = api.getAccounts() ?: return
val builder = val builder =
@ -98,11 +105,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
} }
@UiThread @UiThread
fun addAccount(activity: Activity?, api: AccountManager) { fun addAccount(activity: FragmentActivity?, api: AccountManager) {
try { try {
when (api) { when (api) {
is OAuth2API -> { is OAuth2API -> {
api.authenticate() api.authenticate(activity)
} }
is InAppAuthAPI -> { is InAppAuthAPI -> {
val builder = val builder =
@ -144,13 +151,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.login_username_input?.isVisible = api.requiresUsername dialog.login_username_input?.isVisible = api.requiresUsername
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank()
dialog.create_account?.setOnClickListener { dialog.create_account?.setOnClickListener {
val i = Intent(Intent.ACTION_VIEW) openBrowser(
i.data = Uri.parse(api.createAccountUrl) api.createAccountUrl ?: return@setOnClickListener,
try { activity
activity.startActivity(i) )
} catch (e: Exception) { dialog.dismissSafe()
logError(e)
}
} }
dialog.text1?.text = api.name dialog.text1?.text = api.name
@ -181,7 +186,8 @@ class SettingsAccount : PreferenceFragmentCompat() {
try { try {
showToast( showToast(
activity, activity,
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail)
.format(
api.name api.name
) )
) )

View file

@ -113,6 +113,22 @@ class SettingsPlayer : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true 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 { getPref(R.string.subtitle_settings_key)?.setOnPreferenceClickListener {
SubtitlesFragment.push(activity, false) SubtitlesFragment.push(activity, false)
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@ -86,8 +87,20 @@ class SettingsUI : PreferenceFragmentCompat() {
} }
getPref(R.string.app_theme_key)?.setOnPreferenceClickListener { getPref(R.string.app_theme_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.themes_names) val prefNames = resources.getStringArray(R.array.themes_names).toMutableList()
val prefValues = resources.getStringArray(R.array.themes_names_values) 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 = val currentLayout =
settingsManager.getString(getString(R.string.app_theme_key), prefValues.first()) settingsManager.getString(getString(R.string.app_theme_key), prefValues.first())
@ -110,8 +123,20 @@ class SettingsUI : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
getPref(R.string.primary_color_key)?.setOnPreferenceClickListener { getPref(R.string.primary_color_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.themes_overlay_names) val prefNames = resources.getStringArray(R.array.themes_overlay_names).toMutableList()
val prefValues = resources.getStringArray(R.array.themes_overlay_names_values) 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 = val currentLayout =
settingsManager.getString(getString(R.string.primary_color_key), prefValues.first()) settingsManager.getString(getString(R.string.primary_color_key), prefValues.first())

View file

@ -3,21 +3,19 @@ package com.lagradost.cloudstream3.ui.settings.extensions
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline
import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import kotlinx.coroutines.launch import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
data class RepositoryData( data class RepositoryData(
@JsonProperty("name") val name: String, @JsonProperty("name") val name: String,
@ -46,7 +44,8 @@ class ExtensionsViewModel : ViewModel() {
val pluginStats: LiveData<Some<PluginStats>> = _pluginStats val pluginStats: LiveData<Some<PluginStats>> = _pluginStats
//TODO CACHE GET REQUESTS //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) val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
@ -61,6 +60,7 @@ class ExtensionsViewModel : ViewModel() {
PluginManager.OnlinePluginData(savedData, onlineData) PluginManager.OnlinePluginData(savedData, onlineData)
} }
}.flatten().distinctBy { it.onlineData.second.url } }.flatten().distinctBy { it.onlineData.second.url }
val total = onlinePlugins.count() val total = onlinePlugins.count()
val disabled = outdatedPlugins.count { it.isDisabled } val disabled = outdatedPlugins.count { it.isDisabled }
val downloadedTotal = outdatedPlugins.count() val downloadedTotal = outdatedPlugins.count()

View file

@ -147,7 +147,7 @@ class PluginsFragment : Fragment() {
pluginViewModel.updatePluginListLocal() pluginViewModel.updatePluginListLocal()
tv_types_scroll_view?.isVisible = false tv_types_scroll_view?.isVisible = false
} else { } else {
pluginViewModel.updatePluginList(url) pluginViewModel.updatePluginList(context, url)
tv_types_scroll_view?.isVisible = true tv_types_scroll_view?.isVisible = true
// 💀💀💀💀💀💀💀 Recyclerview when // 💀💀💀💀💀💀💀 Recyclerview when

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.settings.extensions package com.lagradost.cloudstream3.ui.settings.extensions
import android.app.Activity import android.app.Activity
import android.content.Context
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -13,6 +14,7 @@ import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.plugins.PluginData import com.lagradost.cloudstream3.plugins.PluginData
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath
import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.plugins.SitePlugin import com.lagradost.cloudstream3.plugins.SitePlugin
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
@ -45,8 +47,8 @@ class PluginsViewModel : ViewModel() {
private val repositoryCache: MutableMap<String, List<Plugin>> = mutableMapOf() private val repositoryCache: MutableMap<String, List<Plugin>> = mutableMapOf()
const val TAG = "PLG" const val TAG = "PLG"
private fun isDownloaded(plugin: Plugin, data: Set<String>? = null): Boolean { private fun isDownloaded(context: Context, pluginName: String, repositoryUrl: String): Boolean {
return (data ?: getDownloads()).contains(plugin.second.internalName) return getPluginPath(context, pluginName, repositoryUrl).exists()
} }
private suspend fun getPlugins( private suspend fun getPlugins(
@ -63,24 +65,15 @@ class PluginsViewModel : ViewModel() {
?.also { repositoryCache[repositoryUrl] = it } ?: emptyList() ?.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 * @param viewModel optional, updates the plugins livedata for that viewModel if included
* */ * */
fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) =
ioSafe { ioSafe {
if (activity == null) return@ioSafe if (activity == null) return@ioSafe
val stored = getDownloads()
val plugins = getPlugins(repositoryUrl) val plugins = getPlugins(repositoryUrl)
plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list -> plugins.filter { plugin -> !isDownloaded(activity, plugin.second.internalName, repositoryUrl) }.also { list ->
main { main {
showToast( showToast(
activity, activity,
@ -103,7 +96,7 @@ class PluginsViewModel : ViewModel() {
PluginManager.downloadAndLoadPlugin( PluginManager.downloadAndLoadPlugin(
activity, activity,
metadata.url, metadata.url,
metadata.name, metadata.internalName,
repo repo
) )
}.main { list -> }.main { list ->
@ -117,7 +110,7 @@ class PluginsViewModel : ViewModel() {
), ),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
) )
viewModel?.updatePluginListPrivate(repositoryUrl) viewModel?.updatePluginListPrivate(activity, repositoryUrl)
} else if (list.isNotEmpty()) { } else if (list.isNotEmpty()) {
showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT) showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT)
} }
@ -140,11 +133,10 @@ class PluginsViewModel : ViewModel() {
if (activity == null) return@ioSafe if (activity == null) return@ioSafe
val (repo, metadata) = plugin val (repo, metadata) = plugin
val (success, message) = if (isDownloaded(plugin) || isLocal) { val file = getPluginPath(activity, plugin.second.internalName, plugin.first)
PluginManager.deletePlugin(
metadata.url, val (success, message) = if (file.exists() || isLocal) {
isLocal PluginManager.deletePlugin(file) to R.string.plugin_deleted
) to R.string.plugin_deleted
} else { } else {
PluginManager.downloadAndLoadPlugin( PluginManager.downloadAndLoadPlugin(
activity, activity,
@ -165,14 +157,13 @@ class PluginsViewModel : ViewModel() {
if (isLocal) if (isLocal)
updatePluginListLocal() updatePluginListLocal()
else else
updatePluginListPrivate(repositoryUrl) updatePluginListPrivate(activity, repositoryUrl)
} }
private suspend fun updatePluginListPrivate(repositoryUrl: String) { private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) {
val stored = getDownloads()
val plugins = getPlugins(repositoryUrl) val plugins = getPlugins(repositoryUrl)
val list = plugins.map { plugin -> val list = plugins.map { plugin ->
PluginViewData(plugin, isDownloaded(plugin, stored)) PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first))
} }
this.plugins = list this.plugins = list
@ -211,9 +202,10 @@ class PluginsViewModel : ViewModel() {
_filteredPlugins.postValue(false to plugins.filterTvTypes().filterLang().sortByQuery(currentQuery)) _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") Log.i(TAG, "updatePluginList = $repositoryUrl")
updatePluginListPrivate(repositoryUrl) updatePluginListPrivate(context, repositoryUrl)
} }
fun search(query: String?) { fun search(query: String?) {

View file

@ -135,7 +135,8 @@ class SubtitlesFragment : Fragment() {
it.mkdir() it.mkdir()
} }
return fontDir.list()?.mapNotNull { 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) File(fontDir.absolutePath + "/" + it)
} else null } else null
} ?: listOf() } ?: listOf()

View file

@ -31,6 +31,7 @@ import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.toSpanned import androidx.core.text.toSpanned
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -415,7 +416,7 @@ object AppUtils {
} }
} }
fun AppCompatActivity.loadResult( fun FragmentActivity.loadResult(
url: String, url: String,
apiName: String, apiName: String,
startAction: Int = 0, startAction: Int = 0,

View file

@ -344,6 +344,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
VidSrcExtractor(), VidSrcExtractor(),
VidSrcExtractor2(), VidSrcExtractor2(),
PlayLtXyz(), PlayLtXyz(),
AStreamHub(),
) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before After
Before After

View file

@ -420,14 +420,14 @@
<FrameLayout <FrameLayout
android:nextFocusRight="@id/result_bookmark_button"
android:id="@+id/result_movie_progress_downloaded_holder" android:id="@+id/result_movie_progress_downloaded_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:layout_weight="1" android:layout_weight="1"
android:minWidth="250dp"> android:minWidth="250dp"
android:nextFocusRight="@id/result_bookmark_button">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/result_download_movie" android:id="@+id/result_download_movie"
@ -510,17 +510,17 @@
</FrameLayout> </FrameLayout>
<com.google.android.material.button.MaterialButton <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" android:id="@+id/result_bookmark_button"
style="@style/BlackButton" style="@style/BlackButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:layout_weight="1" android:layout_weight="1"
android:minWidth="250dp" 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:text="@string/type_none"
android:visibility="visible" /> android:visibility="visible" />
</LinearLayout> </LinearLayout>
@ -753,6 +753,16 @@
android:orientation="horizontal" android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/result_episode" /> 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>
</LinearLayout> </LinearLayout>

View file

@ -52,6 +52,11 @@
android:id="@+id/home_select_none" android:id="@+id/home_select_none"
style="@style/RoundedSelectableButtonIcon"/>--> style="@style/RoundedSelectableButtonIcon"/>-->
<!--
If you reorder this fix getPairList() too!
That shit is responsible for focus selection
-->
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_movies" android:id="@+id/home_select_movies"

View file

@ -101,7 +101,7 @@
<TextView <TextView
android:id="@+id/settings_extensions" android:id="@+id/settings_extensions"
style="@style/SettingsItem" style="@style/SettingsItem"
android:nextFocusUp="@id/settings_updates" android:nextFocusUp="@id/settings_credits"
android:text="@string/extensions" /> android:text="@string/extensions" />
<LinearLayout <LinearLayout

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

View file

@ -274,7 +274,7 @@
<string name="episode_action_chromecast_episode">حلقة كروم كاست</string> <string name="episode_action_chromecast_episode">حلقة كروم كاست</string>
<string name="episode_action_chromecast_mirror">مرآة كروم كاست</string> <string name="episode_action_chromecast_mirror">مرآة كروم كاست</string>
<string name="episode_action_play_in_app">تشغيل في التطبيق</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_play_in_browser">تشغيل في الويب </string>
<string name="episode_action_copy_link">نسخ الرابط</string> <string name="episode_action_copy_link">نسخ الرابط</string>
<string name="episode_action_auto_download">التحميل التلقائي</string> <string name="episode_action_auto_download">التحميل التلقائي</string>

View file

@ -279,7 +279,7 @@
<string name="episode_action_chromecast_episode">Episódio pelo Chromecast</string> <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_chromecast_mirror">Alternativa pelo Chromecast</string>
<string name="episode_action_play_in_app">Assistir no App</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_play_in_browser">Assistir no navegador</string>
<string name="episode_action_copy_link">Copiar link</string> <string name="episode_action_copy_link">Copiar link</string>
<string name="episode_action_auto_download">Auto download</string> <string name="episode_action_auto_download">Auto download</string>

View file

@ -268,7 +268,7 @@
<string name="episode_action_chromecast_episode">Chromecastovat epizodu</string> <string name="episode_action_chromecast_episode">Chromecastovat epizodu</string>
<string name="episode_action_chromecast_mirror">Chromecast jako zrcadlo</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_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_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_copy_link">Zkopírovat odkaz</string>
<string name="episode_action_auto_download">Automaticky stáhnout</string> <string name="episode_action_auto_download">Automaticky stáhnout</string>

View file

@ -281,7 +281,7 @@
<string name="episode_action_chromecast_episode">Chromecast-Episode</string> <string name="episode_action_chromecast_episode">Chromecast-Episode</string>
<string name="episode_action_chromecast_mirror">Chromecastmirror</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_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_play_in_browser">In Browser wiedergeben</string>
<string name="episode_action_copy_link">Link kopieren</string> <string name="episode_action_copy_link">Link kopieren</string>
<string name="episode_action_auto_download">Auto Download</string> <string name="episode_action_auto_download">Auto Download</string>

View file

@ -14,19 +14,6 @@
<item>@id/cast_button_type_forward_30_seconds</item> <item>@id/cast_button_type_forward_30_seconds</item>
</array> </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"> <array name="limit_title_rez_pref_names">
<item>@string/resolution_and_title</item> <item>@string/resolution_and_title</item>
<item>@string/title</item> <item>@string/title</item>
@ -169,7 +156,7 @@
<item>@string/episode_action_chromecast_episode</item> <item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item> <item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</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_play_in_browser</item>
<item>@string/episode_action_copy_link</item> <item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item> <item>@string/episode_action_auto_download</item>
@ -222,6 +209,8 @@
<item>Banana</item> <item>Banana</item>
<item>Fiesta</item> <item>Fiesta</item>
<item>Dolor rosa</item> <item>Dolor rosa</item>
<item>Material You</item>
<item>Material You (Secondary)</item>
</string-array> </string-array>
<string-array name="themes_overlay_names_values"> <string-array name="themes_overlay_names_values">
<item>Normal</item> <item>Normal</item>
@ -240,19 +229,24 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Monet</item>
<item>Monet2</item>
</string-array> </string-array>
<string-array name="themes_names"> <string-array name="themes_names">
<item>Oscuro</item> <item>Oscuro</item>
<item>Gris</item> <item>Gris</item>
<item>Amoled</item> <item>Amoled</item>
<item>Destello</item> <item>Destello</item>
<item>Material You</item>
</string-array> </string-array>
<string-array name="themes_names_values"> <string-array name="themes_names_values">
<item>AmoledLight</item> <item>AmoledLight</item>
<item>Black</item> <item>Black</item>
<item>Amoled</item> <item>Amoled</item>
<item>Light</item> <item>Light</item>
<item>Monet</item>
</string-array> </string-array>
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266--> <!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->

View file

@ -269,7 +269,7 @@
<string name="episode_action_chromecast_episode">Episodio Chromecast</string> <string name="episode_action_chromecast_episode">Episodio Chromecast</string>
<string name="episode_action_chromecast_mirror">Espejo 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_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_play_in_browser">Reproducir en el navegador</string>
<string name="episode_action_copy_link">Copiar enlace</string> <string name="episode_action_copy_link">Copiar enlace</string>
<string name="episode_action_auto_download">Descarga automática</string> <string name="episode_action_auto_download">Descarga automática</string>

View file

@ -164,7 +164,7 @@
<string name="episode_action_chromecast_episode">Episode Chromecast</string> <string name="episode_action_chromecast_episode">Episode Chromecast</string>
<string name="episode_action_chromecast_mirror">Miroir 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_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_play_in_browser">Lecture dans le navigateur</string>
<string name="episode_action_copy_link">Copier le lien</string> <string name="episode_action_copy_link">Copier le lien</string>
<string name="episode_action_auto_download">Téléchargement Automatique</string> <string name="episode_action_auto_download">Téléchargement Automatique</string>

View file

@ -136,7 +136,7 @@
<string name="episode_action_chromecast_episode">क्रोमकास्ट एपिसोड</string> <string name="episode_action_chromecast_episode">क्रोमकास्ट एपिसोड</string>
<string name="episode_action_chromecast_mirror">कक्रोमकास्ट मिरर</string> <string name="episode_action_chromecast_mirror">कक्रोमकास्ट मिरर</string>
<string name="episode_action_play_in_app">एप्प मैं चलाये</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_play_in_browser">Browser में चलाए</string>
<string name="episode_action_copy_link">लिंक कॉपी करें</string> <string name="episode_action_copy_link">लिंक कॉपी करें</string>
<string name="episode_action_auto_download">डाउनलोड करे</string> <string name="episode_action_auto_download">डाउनलोड करे</string>

View file

@ -299,7 +299,7 @@
<string name="episode_action_chromecast_episode">Chromecast epizoda</string> <string name="episode_action_chromecast_episode">Chromecast epizoda</string>
<string name="episode_action_chromecast_mirror">Chromecast mirror</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_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_play_in_browser">Pokreni u pregledniku</string>
<string name="episode_action_copy_link">Kopiraj poveznicu</string> <string name="episode_action_copy_link">Kopiraj poveznicu</string>
<string name="episode_action_auto_download">Automatsko preuzimanje</string> <string name="episode_action_auto_download">Automatsko preuzimanje</string>

View file

@ -264,7 +264,7 @@
<string name="episode_action_chromecast_episode">Episode Chromecast</string> <string name="episode_action_chromecast_episode">Episode Chromecast</string>
<string name="episode_action_chromecast_mirror">Mirror 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_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_play_in_browser">Putar di browser</string>
<string name="episode_action_copy_link">Salin tautan</string> <string name="episode_action_copy_link">Salin tautan</string>
<string name="episode_action_auto_download">Download otomatis</string> <string name="episode_action_auto_download">Download otomatis</string>

View file

@ -271,7 +271,7 @@
<string name="episode_action_chromecast_episode">Chromecast</string> <string name="episode_action_chromecast_episode">Chromecast</string>
<string name="episode_action_chromecast_mirror">Chromecast mirror</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_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_play_in_browser">Riproduci nel browser</string>
<string name="episode_action_copy_link">Copia link</string> <string name="episode_action_copy_link">Copia link</string>
<string name="episode_action_auto_download">Download</string> <string name="episode_action_auto_download">Download</string>

View file

@ -190,7 +190,7 @@
<string name="episode_action_chromecast_episode">Епизода на Chromecast</string> <string name="episode_action_chromecast_episode">Епизода на Chromecast</string>
<string name="episode_action_chromecast_mirror">Огледало на Chromecastr</string> <string name="episode_action_chromecast_mirror">Огледало на Chromecastr</string>
<string name="episode_action_play_in_app">Пушти во апликацијата</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_play_in_browser">Пушти на прелистувач</string>
<string name="episode_action_copy_link">Копирај линк</string> <string name="episode_action_copy_link">Копирај линк</string>
<string name="episode_action_auto_download">Авто превземање</string> <string name="episode_action_auto_download">Авто превземање</string>

View file

@ -175,7 +175,7 @@
<!-- <string name="episode_action_chomecast_episode">Chromecast Episode</string> <!-- <string name="episode_action_chomecast_episode">Chromecast Episode</string>
<string name="episode_action_chomecast_mirror">Chromecast Mirror</string> --> <string name="episode_action_chomecast_mirror">Chromecast Mirror</string> -->
<string name="episode_action_play_in_app">ആപ്പിൽ പ്ലേയ് ചെയ്യുക</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_play_in_browser">ബ്രൗസറിൽ പ്ലേയ് ചെയ്യുക</string>
<string name="episode_action_copy_link">ലിങ്ക് പകർത്തുക</string> <string name="episode_action_copy_link">ലിങ്ക് പകർത്തുക</string>
<string name="episode_action_auto_download">ഡൌൺലോഡ് ചെയ്യൂ</string> <string name="episode_action_auto_download">ഡൌൺലോഡ് ചെയ്യൂ</string>

View file

@ -145,7 +145,7 @@
<string name="episode_action_chromecast_episode">aauugghhooo-ahah ohaaauugghh</string> <string name="episode_action_chromecast_episode">aauugghhooo-ahah ohaaauugghh</string>
<string name="episode_action_chromecast_mirror">aoohaaahhu ahouuhhh</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_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_play_in_browser">ahoha ooo-ahahohoohah oooohh</string>
<string name="episode_action_copy_link">aauugghhahhaauugghh</string> <string name="episode_action_copy_link">aauugghhahhaauugghh</string>
<string name="episode_action_auto_download">aaaghhoooohh aaahhu ahooo</string> <string name="episode_action_auto_download">aaaghhoooohh aaahhu ahooo</string>

View file

@ -20,7 +20,7 @@
<!-- TRANSLATE, BUT DON'T FORGET FORMAT --> <!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">Snelheid (%.2fx)</string> <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="new_update_format" formatted="true">Nieuwe update gevonden!\n%s -> %s</string>
<string name="filler" formatted="true">Filler</string> <string name="filler" formatted="true">Filler</string>
<string name="duration_format" formatted="true">%d min</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_episode">Chromecast aflevering</string>
<string name="episode_action_chromecast_mirror">Chromecast mirror</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_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_play_in_browser">Speel in browser</string>
<string name="episode_action_copy_link">Kopieer link</string> <string name="episode_action_copy_link">Kopieer link</string>
<string name="episode_action_auto_download">Automatisch downloaden</string> <string name="episode_action_auto_download">Automatisch downloaden</string>

View file

@ -196,7 +196,7 @@
<string name="episode_action_chromecast_episode">Støpt Episode</string> <string name="episode_action_chromecast_episode">Støpt Episode</string>
<string name="episode_action_chromecast_mirror">Støpt Speil</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_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_play_in_browser">Spill i nettleseren</string>
<string name="episode_action_copy_link">Kopier link</string> <string name="episode_action_copy_link">Kopier link</string>
<string name="episode_action_auto_download">Automatisk nedlasting</string> <string name="episode_action_auto_download">Automatisk nedlasting</string>

View file

@ -165,7 +165,7 @@
<item>@string/episode_action_chromecast_episode</item> <item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item> <item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</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_play_in_browser</item>
<item>@string/episode_action_copy_link</item> <item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item> <item>@string/episode_action_auto_download</item>
@ -218,6 +218,8 @@
<item>Bananowy</item> <item>Bananowy</item>
<item>Łososiowy</item> <item>Łososiowy</item>
<item>Świnko peppowy</item> <item>Świnko peppowy</item>
<item>Material You</item>
<item>Material You (drugorzędny)</item>
</string-array> </string-array>
<string-array name="themes_overlay_names_values"> <string-array name="themes_overlay_names_values">
<item>Normal</item> <item>Normal</item>
@ -236,19 +238,24 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Monet</item>
<item>Monet2</item>
</string-array> </string-array>
<string-array name="themes_names"> <string-array name="themes_names">
<item>Ciemny</item> <item>Ciemny</item>
<item>Szary</item> <item>Szary</item>
<item>Amoled</item> <item>Amoled</item>
<item>Flashbang</item> <item>Flashbang</item>
<item>Material You</item>
</string-array> </string-array>
<string-array name="themes_names_values"> <string-array name="themes_names_values">
<item>AmoledLight</item> <item>AmoledLight</item>
<item>Black</item> <item>Black</item>
<item>Amoled</item> <item>Amoled</item>
<item>Light</item> <item>Light</item>
<item>Monet</item>
</string-array> </string-array>
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266--> <!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->

View file

@ -252,7 +252,7 @@
<string name="episode_action_chromecast_episode">Chromecast odcinka</string> <string name="episode_action_chromecast_episode">Chromecast odcinka</string>
<string name="episode_action_chromecast_mirror">Chromecast mirroru</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_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_play_in_browser">Odtwórz w przeglądarce</string>
<string name="episode_action_copy_link">Kopiuj link</string> <string name="episode_action_copy_link">Kopiuj link</string>
<string name="episode_action_auto_download">Automatyczne pobieranie</string> <string name="episode_action_auto_download">Automatyczne pobieranie</string>
@ -455,4 +455,5 @@
<string name="extension_types">Wspierane</string> <string name="extension_types">Wspierane</string>
<string name="extension_language">Język</string> <string name="extension_language">Język</string>
<string name="extension_install_first">Najpierw zainstaluj rozszerzenie</string> <string name="extension_install_first">Najpierw zainstaluj rozszerzenie</string>
<string name="plugins_updated">Zaaktualizowano %d rozszerzeń</string>
</resources> </resources>

View file

@ -268,7 +268,7 @@
<string name="episode_action_chromecast_episode">Episódio pelo Chromecast</string> <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_chromecast_mirror">Alternativa pelo Chromecast</string>
<string name="episode_action_play_in_app">Reproduzir na app</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_play_in_browser">Reproduzir no navegador</string>
<string name="episode_action_copy_link">Copiar link</string> <string name="episode_action_copy_link">Copiar link</string>
<string name="episode_action_auto_download">Transferência Automática</string> <string name="episode_action_auto_download">Transferência Automática</string>

View file

@ -267,7 +267,7 @@
<string name="episode_action_chromecast_episode">Chromecast</string> <string name="episode_action_chromecast_episode">Chromecast</string>
<string name="episode_action_chromecast_mirror">Chromecast alternativ</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_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_play_in_browser">Redă în Browser</string>
<string name="episode_action_copy_link">Copiază link-ul</string> <string name="episode_action_copy_link">Copiază link-ul</string>
<string name="episode_action_auto_download">Auto-descărcare</string> <string name="episode_action_auto_download">Auto-descărcare</string>

View file

@ -167,7 +167,7 @@
<string name="episode_action_chromecast_episode">Chromecasta ett Avsnitt</string> <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_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_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_play_in_browser">Spela upp i webbläsaren</string>
<string name="episode_action_copy_link">Kopiera länk</string> <string name="episode_action_copy_link">Kopiera länk</string>
<string name="episode_action_auto_download">Automatisk nerladdning</string> <string name="episode_action_auto_download">Automatisk nerladdning</string>

View file

@ -204,7 +204,7 @@
<string name="episode_action_chromecast_episode">Chromecast Episode</string> <string name="episode_action_chromecast_episode">Chromecast Episode</string>
<string name="episode_action_chromecast_mirror">Chromecast Mirror</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_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_play_in_browser">I-play sa browser</string>
<string name="episode_action_copy_link">Kopyahin ang Link</string> <string name="episode_action_copy_link">Kopyahin ang Link</string>
<string name="episode_action_auto_download">Awtomatiking i-download</string> <string name="episode_action_auto_download">Awtomatiking i-download</string>

View file

@ -14,19 +14,6 @@
<item>@id/cast_button_type_forward_30_seconds</item> <item>@id/cast_button_type_forward_30_seconds</item>
</array> </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"> <array name="limit_title_rez_pref_names">
<item>@string/resolution_and_title</item> <item>@string/resolution_and_title</item>
<item>@string/title</item> <item>@string/title</item>
@ -169,7 +156,7 @@
<item>@string/episode_action_chromecast_episode</item> <item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item> <item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</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_play_in_browser</item>
<item>@string/episode_action_copy_link</item> <item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item> <item>@string/episode_action_auto_download</item>
@ -222,6 +209,8 @@
<item>Muz</item> <item>Muz</item>
<item>Parti</item> <item>Parti</item>
<item>Pembe</item> <item>Pembe</item>
<item>Material You</item>
<item>Material You (Secondary)</item>
</string-array> </string-array>
<string-array name="themes_overlay_names_values"> <string-array name="themes_overlay_names_values">
<item>Normal</item> <item>Normal</item>
@ -240,19 +229,24 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Monet</item>
<item>Monet2</item>
</string-array> </string-array>
<string-array name="themes_names"> <string-array name="themes_names">
<item>Koyu</item> <item>Koyu</item>
<item>Gri</item> <item>Gri</item>
<item>Amoled</item> <item>Amoled</item>
<item>Flaş Bombası</item> <item>Flaş Bombası</item>
<item>Material You</item>
</string-array> </string-array>
<string-array name="themes_names_values"> <string-array name="themes_names_values">
<item>AmoledLight</item> <item>AmoledLight</item>
<item>Black</item> <item>Black</item>
<item>Amoled</item> <item>Amoled</item>
<item>Light</item> <item>Light</item>
<item>Monet</item>
</string-array> </string-array>
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266--> <!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->

View file

@ -272,7 +272,7 @@
<string name="episode_action_chromecast_episode">Bölümü Chromecast ile yayınla</string> <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_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_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_play_in_browser">Tarayıcıda oynat</string>
<string name="episode_action_copy_link">Linki kopyala</string> <string name="episode_action_copy_link">Linki kopyala</string>
<string name="episode_action_auto_download">Otomatik indir</string> <string name="episode_action_auto_download">Otomatik indir</string>

View file

@ -14,18 +14,6 @@
<item>@id/cast_button_type_forward_30_seconds</item> <item>@id/cast_button_type_forward_30_seconds</item>
</array> </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"> <array name="limit_title_rez_pref_names">
<item>@string/resolution_and_title</item> <item>@string/resolution_and_title</item>
@ -169,7 +157,7 @@
<item>@string/episode_action_chromecast_episode</item> <item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item> <item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</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_play_in_browser</item>
<item>@string/episode_action_copy_link</item> <item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item> <item>@string/episode_action_auto_download</item>
@ -222,6 +210,8 @@
<item>Vàng</item> <item>Vàng</item>
<item>Hồng</item> <item>Hồng</item>
<item>Hồng đậm</item> <item>Hồng đậm</item>
<item>Material You</item>
<item>Material You (Secondary)</item>
</string-array> </string-array>
<string-array name="themes_overlay_names_values"> <string-array name="themes_overlay_names_values">
<item>Normal</item> <item>Normal</item>
@ -240,19 +230,24 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Monet</item>
<item>Monet2</item>
</string-array> </string-array>
<string-array name="themes_names"> <string-array name="themes_names">
<item>Tối</item> <item>Tối</item>
<item>Xám</item> <item>Xám</item>
<item>Amoled</item> <item>Amoled</item>
<item>Sáng</item> <item>Sáng</item>
<item>Material You</item>
</string-array> </string-array>
<string-array name="themes_names_values"> <string-array name="themes_names_values">
<item>AmoledLight</item> <item>AmoledLight</item>
<item>Black</item> <item>Black</item>
<item>Amoled</item> <item>Amoled</item>
<item>Light</item> <item>Light</item>
<item>Monet</item>
</string-array> </string-array>
<!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266--> <!--https://github.com/videolan/vlc-android/blob/72ccfb93db027b49855760001d1a930fa657c5a8/application/resources/src/main/res/values/arrays.xml#L266-->

View file

@ -292,7 +292,7 @@
<string name="episode_action_chromecast_episode">Tập Chromecast</string> <string name="episode_action_chromecast_episode">Tập Chromecast</string>
<string name="episode_action_chromecast_mirror">Chiếu 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_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_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_copy_link">Sao chép liên kết</string>
<string name="episode_action_auto_download">Tự động tải xuống</string> <string name="episode_action_auto_download">Tự động tải xuống</string>

View file

@ -303,7 +303,7 @@
<string name="episode_action_chromecast_episode">投屏剧集</string> <string name="episode_action_chromecast_episode">投屏剧集</string>
<string name="episode_action_chromecast_mirror">投屏镜像</string> <string name="episode_action_chromecast_mirror">投屏镜像</string>
<string name="episode_action_play_in_app">在应用中播放</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_play_in_browser">在浏览器中播放</string>
<string name="episode_action_copy_link">复制链接</string> <string name="episode_action_copy_link">复制链接</string>
<string name="episode_action_auto_download">自动下载</string> <string name="episode_action_auto_download">自动下载</string>

Some files were not shown because too many files have changed in this diff Show more