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