diff --git a/.github/ISSUE_TEMPLATE/application-bug.yml b/.github/ISSUE_TEMPLATE/application-bug.yml
index f3590067312..0b6af0ab0a8 100644
--- a/.github/ISSUE_TEMPLATE/application-bug.yml
+++ b/.github/ISSUE_TEMPLATE/application-bug.yml
@@ -86,7 +86,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- - label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
+ - label: I have updated the app to pre-release version **[Latest](https://github.com/3a4oT/cloudstream/releases)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
diff --git a/.github/locales.py b/.github/locales.py
index dd23ebbe225..a74d7258834 100644
--- a/.github/locales.py
+++ b/.github/locales.py
@@ -1,6 +1,7 @@
import re
import glob
import requests
+import os
import lxml.etree as ET # builtin library doesn't preserve comments
@@ -61,5 +62,8 @@
with open(file, 'wb') as fp:
fp.write(b'\n')
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
+ # Remove trailing new line to be consistent with weblate
+ fp.seek(-1, os.SEEK_END)
+ fp.truncate()
except ET.ParseError as ex:
print(f"[{file}] {ex}")
diff --git a/README.md b/README.md
index 8949304e94c..e26286f6838 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,18 @@
-# CloudStream
+# Модифікований клієнт [CloudStream](https://github.com/recloudstream/cloudstream)
-**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
+
+## 📖 Що це таке?
+Це спеціальна програма для OS Android iз запропонованим розширенням (ви самі маєте вирішити чи активувати запропоноване розширення) для перегляду фільмів, серіалів та аніме в якісному українському дубляжу від різних постачальників в стрімінговій програмі [Cloudstream](https://github.com/recloudstream/cloudstream). Підтримує AndroidTV
-[](https://discord.gg/5Hus6fM)
+## 👨🏻🔧 Як завантажити?
+Можна [завантажити APK файл](https://github.com/3a4oT/cloudstream/releases/download/4.3.333/stream-UKR-4-3-333.apk). Або зібрати самому із вихідного коду.
-### Features:
-+ **AdFree**, No ads whatsoever
-+ No tracking/analytics
-+ Bookmarks
-+ Phone and TV support
-+ Chromecast
-+ Extension system for personal customization
+## ⚙️ Активація розширення
+При першому запуску, у вас з'явиться екран налаштування. Після вибору мови, ви потрапите на екран `Розширення` де буде опція під назвою - **"піратити - це погано, ризикуєш? :)"**. Все що вам потрібно - це натиснути на кнопку завантаження, розширення встановиться автоматично. (якщо пропустили цей екран - перейдіть в `Додаток -> Параметри -> Розширення`)
+
+## 🇺🇦 Як відрізняється від оригінального клієнта **CloudStream**
+- Вирізані всі локалізації окрім української та англійської
+- Модифіковно розширення за замовчуванням. Використано форк розширення від [CakesTwix](https://github.com/CakesTwix/cloudstream-extensions-uk). Велика дяка за роботу :))
+- Змінено URL для функції автооновлення.
-### Supported languages:
-
-
-
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6e439d53858..956909a948b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -61,7 +61,7 @@ android {
targetSdk = 33 /* Android 14 is Fu*ked
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
versionCode = 63
- versionName = "4.3.2"
+ versionName = "4.3.333"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@@ -290,7 +290,7 @@ tasks.withType().configureEach {
localDirectory.set(file("src/main/java"))
// URL showing where the source code can be accessed through the web browser
- remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
+ remoteUrl.set(URL("https://github.com/3a4oT/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")
diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
index faacdf50141..c7f02baff8a 100644
--- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
@@ -154,7 +154,7 @@ class ExampleInstrumentedTest {
fun providerCorrectHomepage() {
runBlocking {
getAllProviders().toList().amap { api ->
- TestingUtils.testHomepage(api, ::println)
+ TestingUtils.testHomepage(api, TestingUtils.Logger())
}
}
println("Done providerCorrectHomepage")
@@ -166,7 +166,6 @@ class ExampleInstrumentedTest {
TestingUtils.getDeferredProviderTests(
this,
getAllProviders(),
- ::println
) { _, _ -> }
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a23ef72588f..888be999025 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -97,7 +97,7 @@
-->
Unit)) :
ACRA.errorReporter.handleException(error)
try {
PrintStream(errorFile).use { ps ->
- 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("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
+ ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
error.printStackTrace(ps)
}
} catch (ignored: FileNotFoundException) {
@@ -106,7 +101,6 @@ class AcraApplication : Application() {
override fun onCreate() {
super.onCreate()
- //NativeCrashHandler.initCrashHandler()
ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component))
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
index ba303feff7e..ee3a5d122c7 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -5,6 +5,7 @@ import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
import android.content.pm.PackageManager
+import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.util.DisplayMetrics
@@ -164,7 +165,7 @@ object CommonActivity {
val toast = Toast(act)
toast.duration = duration ?: Toast.LENGTH_SHORT
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
- toast.view = binding.root
+ toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
currentToast = toast
toast.show()
@@ -276,12 +277,35 @@ object CommonActivity {
}
}
+ fun updateTheme(act: Activity) {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
+ if (settingsManager
+ .getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System"
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ loadThemes(act)
+ }
+ }
+
+ private fun mapSystemTheme(act: Activity): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val currentNightMode =
+ act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+ return when (currentNightMode) {
+ Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme
+ else -> R.style.AppTheme // Night mode is active, we're using dark theme
+ }
+ } else {
+ return R.style.AppTheme
+ }
+ }
+
fun loadThemes(act: Activity?) {
if (act == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
val currentTheme =
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
+ "System" -> mapSystemTheme(act)
"Black" -> R.style.AppTheme
"Light" -> R.style.LightMode
"Amoled" -> R.style.AmoledMode
@@ -352,8 +376,8 @@ object CommonActivity {
currentLook = currentLook.parent as? View ?: break
}*/
- private fun View.hasContent() : Boolean {
- return isShown && when(this) {
+ private fun View.hasContent(): Boolean {
+ return isShown && when (this) {
//is RecyclerView -> this.childCount > 0
is ViewGroup -> this.childCount > 0
else -> true
@@ -464,20 +488,6 @@ object CommonActivity {
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
- //println("Keycode: $keyCode")
- //showToast(
- // this,
- // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
- // Toast.LENGTH_LONG
- //)
-
- // Tested keycodes on remote:
- // KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
- // KeyEvent.KEYCODE_MEDIA_REWIND
- // KeyEvent.KEYCODE_MENU
- // KeyEvent.KEYCODE_MEDIA_NEXT
- // KeyEvent.KEYCODE_MEDIA_PREVIOUS
- // KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
// 149 keycode_numpad 5
when (keyCode) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
index 934dd58a351..8da7ca384b5 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
@@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
- private val client: OkHttpClient
+ private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build()
override fun execute(request: Request): Response {
val httpMethod: String = request.httpMethod()
val url: String = request.url()
@@ -74,8 +74,4 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
return instance
}
}
-
- init {
- client = builder.readTimeout(30, TimeUnit.SECONDS).build()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 59f499c5f2e..5408d2a893b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -68,6 +68,7 @@ import com.lagradost.cloudstream3.CommonActivity.screenHeight
import com.lagradost.cloudstream3.CommonActivity.setActivityInstance
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
+import com.lagradost.cloudstream3.CommonActivity.updateTheme
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
@@ -82,13 +83,13 @@ import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
@@ -107,6 +108,7 @@ import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.SyncViewModel
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setText
+import com.lagradost.cloudstream3.ui.result.setTextHtml
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
@@ -131,6 +133,8 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers
+import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
+import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback
import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback
@@ -149,6 +153,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
+import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@@ -346,7 +351,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
println("Repository url: $realUrl")
loadRepository(realUrl)
return true
- } else if (str.contains(appString)) {
+ } else if (str.contains(APP_STRING)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
ioSafe {
@@ -376,15 +381,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
// This specific intent is used for the gradle deployWithAdb
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
- if (str == "$appString:") {
+ if (str == "$APP_STRING:") {
PluginManager.hotReloadAllLocalPlugins(activity)
}
- } else if (safeURI(str)?.scheme == appStringRepo) {
- val url = str.replaceFirst(appStringRepo, "https")
+ } else if (safeURI(str)?.scheme == APP_STRING_REPO) {
+ val url = str.replaceFirst(APP_STRING_REPO, "https")
loadRepository(url)
return true
- } else if (safeURI(str)?.scheme == appStringSearch) {
- val query = str.substringAfter("$appStringSearch://")
+ } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) {
+ val query = str.substringAfter("$APP_STRING_SEARCH://")
nextSearchQuery =
try {
URLDecoder.decode(query, "UTF-8")
@@ -398,7 +403,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
R.id.navigation_search
activity?.findViewById(R.id.nav_rail_view)?.selectedItemId =
R.id.navigation_search
- } else if (safeURI(str)?.scheme == appStringPlayer) {
+ } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) {
val uri = Uri.parse(str)
val name = uri.getQueryParameter("name")
val url = URLDecoder.decode(uri.authority, "UTF-8")
@@ -412,9 +417,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
)
)
)
- } else if (safeURI(str)?.scheme == appStringResumeWatching) {
+ } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) {
val id =
- str.substringAfter("$appStringResumeWatching://").toIntOrNull()
+ str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull()
?: return false
ioSafe {
val resumeWatchingCard =
@@ -468,7 +473,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
) DubStatus.Dubbed else DubStatus.Subbed, null
)
} else {
- viewModel.loadSmall(this, result)
+ viewModel.loadSmall(result)
}
}
@@ -483,6 +488,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateLocale() // android fucks me by chaining lang when rotating the phone
+ updateTheme(this) // Update if system theme
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
@@ -571,11 +577,40 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
binding?.apply {
navRailView.isVisible = isNavVisible && landscape
navView.isVisible = isNavVisible && !landscape
+
+ /**
+ * We need to make sure if we return to a sub-fragment,
+ * the correct navigation item is selected so that it does not
+ * highlight the wrong one in UI.
+ */
+ when (destination.id) {
+ in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> {
+ navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true
+ navView.menu.findItem(R.id.navigation_downloads).isChecked = true
+ }
+ in listOf(
+ R.id.navigation_settings,
+ R.id.navigation_subtitles,
+ R.id.navigation_chrome_subtitles,
+ R.id.navigation_settings_player,
+ R.id.navigation_settings_updates,
+ R.id.navigation_settings_ui,
+ R.id.navigation_settings_account,
+ R.id.navigation_settings_providers,
+ R.id.navigation_settings_general,
+ R.id.navigation_settings_extensions,
+ R.id.navigation_settings_plugins,
+ R.id.navigation_test_providers
+ ) -> {
+ navRailView.menu.findItem(R.id.navigation_settings).isChecked = true
+ navView.menu.findItem(R.id.navigation_settings).isChecked = true
+ }
+ }
}
}
//private var mCastSession: CastSession? = null
- lateinit var mSessionManager: SessionManager
+ var mSessionManager: SessionManager? = null
private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() }
private inner class SessionManagerListenerImpl : SessionManagerListener {
@@ -615,8 +650,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
setActivityInstance(this)
try {
if (isCastApiAvailable()) {
- //mCastSession = mSessionManager.currentCastSession
- mSessionManager.addSessionManagerListener(mSessionManagerListener)
+ mSessionManager?.addSessionManagerListener(mSessionManagerListener)
}
} catch (e: Exception) {
logError(e)
@@ -632,7 +666,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
try {
if (isCastApiAvailable()) {
- mSessionManager.removeSessionManagerListener(mSessionManagerListener)
+ mSessionManager?.removeSessionManagerListener(mSessionManagerListener)
//mCastSession = null
}
} catch (e: Exception) {
@@ -736,7 +770,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let {
- allProviders.add(it.javaClass.newInstance().apply {
+ allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply {
name = custom.name
lang = custom.lang
mainUrl = custom.url.trimEnd('/')
@@ -1117,7 +1151,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
super.onCreate(savedInstanceState)
try {
if (isCastApiAvailable()) {
- mSessionManager = CastContext.getSharedInstance(this).sessionManager
+ CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager }
}
} catch (t: Throwable) {
logError(t)
@@ -1223,17 +1257,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
this.setKey(getString(R.string.jsdelivr_proxy_key), false)
} else {
this.setKey(getString(R.string.jsdelivr_proxy_key), true)
- val parentView: View = findViewById(android.R.id.content)
- Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG)
- .let { snackbar ->
- snackbar.setAction(R.string.revert) {
- setKey(getString(R.string.jsdelivr_proxy_key), false)
- }
- snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground))
- snackbar.setTextColor(colorFromAttribute(R.attr.textColor))
- snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary))
- snackbar.show()
- }
+ showSnackbar(
+ this@MainActivity,
+ R.string.jsdelivr_enabled,
+ Snackbar.LENGTH_LONG,
+ R.string.revert
+ ) { setKey(getString(R.string.jsdelivr_proxy_key), false) }
}
}
}
@@ -1404,7 +1433,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
resultviewPreviewMetaDuration.setText(d.durationText)
resultviewPreviewMetaRating.setText(d.ratingText)
- resultviewPreviewDescription.setText(d.plotText)
+ resultviewPreviewDescription.setTextHtml(d.plotText)
resultviewPreviewPoster.setImage(
d.posterImage ?: d.posterBackgroundImage
)
@@ -1419,13 +1448,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
val value = viewModel.watchStatus.value ?: WatchType.NONE
this@MainActivity.showBottomDialog(
- WatchType.values().map { getString(it.stringRes) }.toList(),
+ WatchType.entries.map { getString(it.stringRes) }.toList(),
value.ordinal,
this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(
- WatchType.values()[it],
+ WatchType.entries[it],
this@MainActivity
)
}
@@ -1435,12 +1464,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
?: SyncWatchType.NONE
this@MainActivity.showBottomDialog(
- SyncWatchType.values().map { getString(it.stringRes) }.toList(),
+ SyncWatchType.entries.map { getString(it.stringRes) }.toList(),
value.ordinal,
this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
- syncViewModel.setStatus(SyncWatchType.values()[it].internalId)
+ syncViewModel.setStatus(SyncWatchType.entries[it].internalId)
syncViewModel.publishUserData()
}
}
@@ -1572,7 +1601,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (isLayout(TV or EMULATOR)) {
if (navDestination.matchDestination(R.id.navigation_home)) {
- attachBackPressedCallback()
+ attachBackPressedCallback {
+ showConfirmExitDialog()
+ window?.navigationBarColor =
+ colorFromAttribute(R.attr.primaryGrayBackground)
+ updateLocale()
+ }
} else detachBackPressedCallback()
}
}
@@ -1817,28 +1851,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
finish()
}
- private var backPressedCallback: OnBackPressedCallback? = null
-
- private fun attachBackPressedCallback() {
- if (backPressedCallback == null) {
- backPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- showConfirmExitDialog()
- window?.navigationBarColor =
- colorFromAttribute(R.attr.primaryGrayBackground)
- updateLocale()
- }
- }
- }
-
- backPressedCallback?.isEnabled = true
- onBackPressedDispatcher.addCallback(this, backPressedCallback ?: return)
- }
-
- private fun detachBackPressedCallback() {
- backPressedCallback?.isEnabled = false
- }
-
suspend fun checkGithubConnectivity(): Boolean {
return try {
app.get(
@@ -1849,4 +1861,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
false
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt
deleted file mode 100644
index 7be904405a9..00000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.lagradost.cloudstream3
-
-import com.lagradost.cloudstream3.MainActivity.Companion.lastError
-import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-object NativeCrashHandler {
- // external fun triggerNativeCrash()
- /*private external fun initNativeCrashHandler()
- private external fun getSignalStatus(): Int
-
- private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
-
- //launch {
- // delay(10000)
- // triggerNativeCrash()
- //}
-
- while (true) {
- delay(10_000)
- val signal = getSignalStatus()
- // Signal is initialized to zero
- if (signal == 0) continue
-
- // Do not crash in safe mode!
- if (lastError != null) continue
- if (checkSafeModeFile()) continue
-
- AcraApplication.exceptionHandler?.uncaughtException(
- Thread.currentThread(),
- RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n")
- )
- }
- }
-
- fun initCrashHandler() {
- try {
- System.loadLibrary("native-lib")
- initNativeCrashHandler()
- } catch (t: Throwable) {
- // Make debug crash.
- if (BuildConfig.DEBUG) throw t
- logError(t)
- return
- }
-
- initSignalPolling()
- }*/
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
index 75e96bec137..bc646a8d2a2 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
@@ -2,15 +2,13 @@ package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncIdName
object SyncRedirector {
- val syncApis = SyncApis
private val syncIds =
listOf(
- SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
- SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
+ SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""),
+ SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""")
)
suspend fun redirect(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
index 7c375e0af6f..addee9a02b7 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
@@ -236,6 +236,7 @@ open class TraktProvider : MainAPI() {
posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
rating = episode.rating?.times(10)?.roundToInt(),
description = episode.overview,
+ runTime = episode.runtime
).apply {
this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) {
@@ -295,7 +296,7 @@ open class TraktProvider : MainAPI() {
return try {
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
- APIHolder.unixTimeMS < dateTime
+ unixTimeMS < dateTime
} catch (t: Throwable) {
logError(t)
false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
index ce2fb3a216d..85a9db5db73 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
@@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.network
-import android.util.Base64
import android.util.Log
import android.webkit.CookieManager
import androidx.annotation.AnyThread
@@ -10,7 +9,10 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.nicehttp.Requests.Companion.await
import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking
-import okhttp3.*
+import okhttp3.Headers
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
import java.net.URI
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
index e89ccfeb316..ddf5b286870 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
@@ -2,5 +2,4 @@ package com.lagradost.cloudstream3.plugins
@Suppress("unused")
@Target(AnnotationTarget.CLASS)
-annotation class CloudstreamPlugin(
-)
\ No newline at end of file
+annotation class CloudstreamPlugin
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
index 7f08af924ad..fc8365876bf 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
@@ -34,7 +34,7 @@ abstract class Plugin {
*/
fun registerMainAPI(element: MainAPI) {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
- element.sourcePlugin = this.__filename
+ element.sourcePlugin = this.filename
// Race condition causing which would case duplicates if not for distinctBy
synchronized(APIHolder.allProviders) {
APIHolder.allProviders.add(element)
@@ -48,7 +48,7 @@ abstract class Plugin {
*/
fun registerExtractorAPI(element: ExtractorApi) {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi")
- element.sourcePlugin = this.__filename
+ element.sourcePlugin = this.filename
extractorApis.add(element)
}
@@ -68,7 +68,11 @@ abstract class Plugin {
*/
var resources: Resources? = null
/** Full file path to the plugin. */
- var __filename: String? = null
+ @Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename"))
+ var __filename: String?
+ get() = filename
+ set(value) {filename = value}
+ var filename: String? = null
/**
* This will add a button in the settings allowing you to add custom settings
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
index 6b2b75f25a2..bc2a1780314 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
@@ -1,13 +1,16 @@
package com.lagradost.cloudstream3.plugins
+import android.Manifest
import android.app.*
import android.content.Context
+import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.content.res.Resources
import android.os.Build
import android.os.Environment
import android.util.Log
import android.widget.Toast
+import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.FragmentActivity
@@ -163,7 +166,7 @@ object PluginManager {
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
- public var currentlyLoading: String? = null
+ var currentlyLoading: String? = null
// Maps filepath to plugin
val plugins: MutableMap =
@@ -339,7 +342,7 @@ object PluginManager {
//Omit non-NSFW if mode is set to NSFW only
if (mode == AutoDownloadMode.NsfwOnly) {
- if (tvtypes.contains(TvType.NSFW.name) == false) {
+ if (!tvtypes.contains(TvType.NSFW.name)) {
return@mapNotNull null
}
}
@@ -504,10 +507,12 @@ object PluginManager {
val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also {
Log.d(TAG, "No manifest version for ${data.internalName}")
}
+
+ @Suppress("UNCHECKED_CAST")
val pluginClass: Class<*> =
loader.loadClass(manifest.pluginClassName) as Class
val pluginInstance: Plugin =
- pluginClass.newInstance() as Plugin
+ pluginClass.getDeclaredConstructor().newInstance() as Plugin
// Sets with the proper version
setPluginData(data.copy(version = version))
@@ -517,14 +522,16 @@ object PluginManager {
return true
}
- pluginInstance.__filename = file.absolutePath
+ pluginInstance.filename = file.absolutePath
if (manifest.requiresResources) {
Log.d(TAG, "Loading resources for ${data.internalName}")
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
- val assets = AssetManager::class.java.newInstance()
+ val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()
val addAssetPath =
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
addAssetPath.invoke(assets, file.absolutePath)
+
+ @Suppress("DEPRECATION")
pluginInstance.resources = Resources(
assets,
context.resources.displayMetrics,
@@ -566,14 +573,14 @@ object PluginManager {
// remove all registered apis
synchronized(APIHolder.apis) {
- APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
+ APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it)
}
}
synchronized(APIHolder.allProviders) {
- APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
+ APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
}
- extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
+ extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
classLoaders.values.removeIf { v -> v == plugin }
@@ -720,9 +727,14 @@ object PluginManager {
}
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)
+ // notificationId is a unique int for each notification that you must define
+ if (ActivityCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ NotificationManagerCompat.from(context)
+ .notify((System.currentTimeMillis() / 1000).toInt(), notification)
}
return notification
} catch (e: Exception) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
index b80a590ef30..fd3712ba56a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
@@ -71,9 +71,15 @@ data class SitePlugin(
object RepositoryManager {
const val ONLINE_PLUGINS_FOLDER = "Extensions"
val PREBUILT_REPOSITORIES: Array by lazy {
- getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
+ getKey("PREBUILT_REPOSITORIES") ?:
+ arrayOf(
+ RepositoryData(
+ name = "піратити це - погано, ризикуєш? :)",
+ url = "https://raw.githubusercontent.com/3a4oT/cloudstream-extensions-uk/master/repo.json"
+ )
+ )
}
- val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
+ private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
fun convertRawGitUrl(url: String): String {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
index a45ab5f0239..d1b702f4ce3 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
@@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock
object VotingApi { // please do not cheat the votes lol
private const val LOGKEY = "VotingApi"
- private const val apiDomain = "https://counterapi.com/api"
+ private const val API_DOMAIN = "https://counterapi.com/api"
private fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest
@@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol
.joinToString("-")
private suspend fun readVote(pluginUrl: String): Int {
- var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
+ val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
Log.d(LOGKEY, "Requesting: $url")
return app.get(url).parsedSafe()?.value ?: 0
}
private suspend fun writeVote(pluginUrl: String): Boolean {
- var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
+ val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
Log.d(LOGKEY, "Requesting: $url")
return app.get(url).parsedSafe()?.value != null
}
@@ -69,8 +69,7 @@ object VotingApi { // please do not cheat the votes lol
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
fun canVote(pluginUrl: String): Boolean {
- if (!PluginManager.urlPlugins.contains(pluginUrl)) return false
- return true
+ return PluginManager.urlPlugins.contains(pluginUrl)
}
private val voteLock = Mutex()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
index 857fba11676..df64caabc9f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
@@ -59,7 +59,7 @@ class SubtitleResource {
return file
}
- fun unzip(file: File): List> {
+ private fun unzip(file: File): List> {
val entries = mutableListOf>()
ZipInputStream(file.inputStream()).use { zipInputStream ->
diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
index ed4ccb74a9b..685b499bb59 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
@@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.subtitles
-import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.TvType
class AbstractSubtitleEntities {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
index 0259ccad35a..2e14c3c46fd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
@@ -56,22 +56,22 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
subSourceApi
)
- const val appString = "cloudstreamapp"
- const val appStringRepo = "cloudstreamrepo"
- const val appStringPlayer = "cloudstreamplayer"
+ const val APP_STRING = "cloudstreamapp"
+ const val APP_STRING_REPO = "cloudstreamrepo"
+ const val APP_STRING_PLAYER = "cloudstreamplayer"
// Instantly start the search given a query
- const val appStringSearch = "cloudstreamsearch"
+ const val APP_STRING_SEARCH = "cloudstreamsearch"
// Instantly resume watching a show
- const val appStringResumeWatching = "cloudstreamcontinuewatching"
+ const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching"
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long
get() = System.currentTimeMillis()
- const val maxStale = 60 * 10
+ const val MAX_STALE = 60 * 10
fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt
index 878e0cb3bfe..dcb8bbead06 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt
@@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
+import java.util.Date
interface SyncAPI : OAuth2API {
/**
@@ -124,6 +125,8 @@ interface SyncAPI : OAuth2API {
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
+ ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate }
+ ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate }
else -> items
}
}
@@ -158,9 +161,10 @@ interface SyncAPI : OAuth2API {
override var posterUrl: String?,
override var posterHeaders: Map?,
override var quality: SearchQuality?,
+ val releaseDate: Date?,
override var id: Int? = null,
val plot : String? = null,
val rating: Int? = null,
- val tags: List? = null,
+ val tags: List? = null
) : SearchResponse
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
index 507c5e2acf7..db4676393ab 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
@@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi {
override fun logOut() {}
companion object {
- const val host = "https://www.addic7ed.com"
+ const val HOST = "https://www.addic7ed.com"
const val TAG = "ADDIC7ED"
}
private fun fixUrl(url: String): String {
- return if (url.startsWith("/")) host + url
- else if (!url.startsWith("http")) "$host/$url"
+ return if (url.startsWith("/")) HOST + url
+ else if (!url.startsWith("http")) "$HOST/$url"
else url
}
@@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi {
}
val title = queryText.substringBefore("(").trim()
- val url = "$host/search.php?search=${title}&Submit=Search"
+ val url = "$HOST/search.php?search=${title}&Submit=Search"
val hostDocument = app.get(url).document
var searchResult = ""
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
@@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi {
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
?.substringBefore(",")
val doc = app.get(
- "$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
- referer = "$host/"
+ "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
+ referer = "$HOST/"
).document
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
if (node.selectFirst("td")?.text()
@@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi {
val link = fixUrl(node.select("a.buttonDownload").attr("href"))
val isHearingImpaired =
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
- cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired)
+ cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired)
}
return results
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
index 8a82cf94e5d..6112c7dbede 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
@@ -16,15 +16,16 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
-import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
+import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
import java.net.URL
import java.net.URLEncoder
-import java.util.*
+import java.util.Locale
class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override var name = "AniList"
@@ -63,7 +64,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun handleRedirect(url: String): Boolean {
val sanitizer =
- splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
+ splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
val token = sanitizer["access_token"]!!
val expiresIn = sanitizer["expires_in"]!!
@@ -87,7 +88,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun search(name: String): List? {
val data = searchShows(name) ?: return null
- return data.data?.Page?.media?.map {
+ return data.data?.page?.media?.map {
SyncAPI.SyncSearchResult(
it.title.romaji ?: return null,
this.name,
@@ -101,7 +102,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getResult(id: String): SyncAPI.SyncResult {
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
- val season = getSeason(internalId).data.Media
+ val season = getSeason(internalId).data.media
return SyncAPI.SyncResult(
season.id.toString(),
@@ -301,12 +302,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
val shows = searchShows(name.replace(blackListRegex, ""))
- shows?.data?.Page?.media?.find {
+ shows?.data?.page?.media?.find {
(malId ?: "NONE") == it.idMal.toString()
}?.let { return it }
val filtered =
- shows?.data?.Page?.media?.filter {
+ shows?.data?.page?.media?.filter {
(((it.startDate.year ?: year.toString()) == year.toString()
|| year == null))
}
@@ -496,7 +497,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val data = postApi(q, true)
val d = parseJson(data ?: return null)
- val main = d.data?.Media
+ val main = d.data?.media
if (main?.mediaListEntry != null) {
return AniListTitleHolder(
title = main.title,
@@ -536,7 +537,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
headers = mapOf(
"Authorization" to "Bearer " + (getAuth()
?: return@suspendSafeApiCall null),
- if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
+ if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache"
),
cacheTime = 0,
data = mapOf(
@@ -631,8 +632,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
?: this.media.coverImage.medium,
null,
null,
+ this.media.seasonYear.toYear(),
null,
- plot = this.media.description
+ plot = this.media.description,
)
}
}
@@ -647,7 +649,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class Data(
- @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
+ @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection
)
private fun getAniListListCached(): Array? {
@@ -659,7 +661,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
if (checkToken()) return null
return if (requireLibraryRefresh) {
- val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray()
+ val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray()
if (list != null) {
setKey(ANILIST_CACHED_LIST, list)
}
@@ -678,7 +680,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
// To fill empty lists when AniList does not return them
val baseMap =
- AniListStatusType.values().filter { it.value >= 0 }.associate {
+ AniListStatusType.entries.filter { it.value >= 0 }.associate {
it.stringRes to emptyList()
}
@@ -689,6 +691,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
+ ListSorting.ReleaseDateNew,
+ ListSorting.ReleaseDateOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
@@ -764,7 +768,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
/** Used to query a saved MediaItem on the list to get the id for removal */
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
- data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null)
+ data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null)
data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId(
@@ -787,7 +791,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
"""
val response = postApi(idQuery)
val listId =
- tryParseJson(response)?.data?.MediaList?.id ?: return false
+ tryParseJson(response)?.data?.mediaList?.id ?: return false
"""
mutation(${'$'}id: Int = $listId) {
DeleteMediaListEntry(id: ${'$'}id) {
@@ -836,7 +840,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val data = postApi(q)
if (data.isNullOrBlank()) return null
val userData = parseJson(data)
- val u = userData.data?.Viewer
+ val u = userData.data?.viewer
val user = AniListUser(
u?.id,
u?.name,
@@ -858,8 +862,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
suspend fun getSeasonRecursive(id: Int) {
val season = getSeason(id)
seasons.add(season)
- if (season.data.Media.format?.startsWith("TV") == true) {
- season.data.Media.relations?.edges?.forEach {
+ if (season.data.media.format?.startsWith("TV") == true) {
+ season.data.media.relations?.edges?.forEach {
if (it.node?.format != null) {
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
getSeasonRecursive(it.node.id)
@@ -878,7 +882,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class SeasonData(
- @JsonProperty("Media") val Media: SeasonMedia,
+ @JsonProperty("Media") val media: SeasonMedia,
)
data class SeasonMedia(
@@ -1050,7 +1054,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class AniListData(
- @JsonProperty("Viewer") val Viewer: AniListViewer?,
+ @JsonProperty("Viewer") val viewer: AniListViewer?,
)
data class AniListRoot(
@@ -1090,7 +1094,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class LikeData(
- @JsonProperty("Viewer") val Viewer: LikeViewer?,
+ @JsonProperty("Viewer") val viewer: LikeViewer?,
)
data class LikeRoot(
@@ -1130,7 +1134,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class GetDataData(
- @JsonProperty("Media") val Media: GetDataMedia?,
+ @JsonProperty("Media") val media: GetDataMedia?,
)
data class GetDataRoot(
@@ -1163,7 +1167,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class GetSearchPage(
- @JsonProperty("Page") val Page: GetSearchData?,
+ @JsonProperty("Page") val page: GetSearchData?,
)
data class GetSearchData(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
index 00f8d00cfb8..0d9a4d1383d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
@@ -119,8 +119,11 @@ class LocalList : SyncAPI {
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
+ ListSorting.ReleaseDateNew,
+ ListSorting.ReleaseDateOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
+
)
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt
index 24ef7136bda..08c1865311e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt
@@ -19,14 +19,19 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
-import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL
import java.security.SecureRandom
import java.text.ParseException
import java.text.SimpleDateFormat
-import java.util.*
+import java.time.Instant
+import java.time.format.DateTimeFormatter
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
/** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */
const val MAL_MAX_SEARCH_LIMIT = 25
@@ -51,7 +56,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
override fun loginInfo(): AuthAPI.LoginInfo? {
- //getMalUser(true)?
getKey(accountId, MAL_USER_KEY)?.let { user ->
return AuthAPI.LoginInfo(
profilePicture = user.picture,
@@ -84,7 +88,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
this.name,
node.id.toString(),
"$mainUrl/anime/${node.id}/",
- node.main_picture?.large ?: node.main_picture?.medium
+ node.mainPicture?.large ?: node.mainPicture?.medium
)
}
}
@@ -178,7 +182,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private fun parseDate(string: String?): Long? {
return try {
- SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time
+ SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time
} catch (e: Exception) {
null
}
@@ -190,7 +194,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
apiName = this.name,
syncId = node.id.toString(),
url = "$mainUrl/anime/${node.id}",
- posterUrl = node.main_picture?.large
+ posterUrl = node.mainPicture?.large
)
}
@@ -244,12 +248,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val internalId = id.toIntOrNull() ?: return null
val data =
- getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status")
+ getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status")
return SyncAPI.SyncStatus(
score = data?.score,
- status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) ,
+ status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)),
isFavorite = null,
- watchedEpisodes = data?.num_episodes_watched,
+ watchedEpisodes = data?.numEpisodesWatched,
)
}
@@ -291,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private fun parseDateLong(string: String?): Long? {
return try {
- SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse(
string ?: return null
)?.time?.div(1000)
} catch (e: Exception) {
@@ -302,7 +306,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun handleRedirect(url: String): Boolean {
val sanitizer =
- splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
+ splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
val state = sanitizer["state"]!!
if (state == "RequestID$requestId") {
val currentCode = sanitizer["code"]!!
@@ -351,9 +355,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
try {
if (response != "") {
val token = parseJson(response)
- setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
- setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
- setKey(accountId, MAL_TOKEN_KEY, token.access_token)
+ setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime))
+ setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken)
+ setKey(accountId, MAL_TOKEN_KEY, token.accessToken)
requireLibraryRefresh = true
}
} catch (e: Exception) {
@@ -395,56 +399,62 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class Node(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String,
- @JsonProperty("main_picture") val main_picture: MainPicture?,
- @JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?,
- @JsonProperty("media_type") val media_type: String?,
- @JsonProperty("num_episodes") val num_episodes: Int?,
+ @JsonProperty("main_picture") val mainPicture: MainPicture?,
+ @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?,
+ @JsonProperty("media_type") val mediaType: String?,
+ @JsonProperty("num_episodes") val numEpisodes: Int?,
@JsonProperty("status") val status: String?,
- @JsonProperty("start_date") val start_date: String?,
- @JsonProperty("end_date") val end_date: String?,
- @JsonProperty("average_episode_duration") val average_episode_duration: Int?,
+ @JsonProperty("start_date") val startDate: String?,
+ @JsonProperty("end_date") val endDate: String?,
+ @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?,
@JsonProperty("synopsis") val synopsis: String?,
@JsonProperty("mean") val mean: Double?,
@JsonProperty("genres") val genres: List?,
@JsonProperty("rank") val rank: Int?,
@JsonProperty("popularity") val popularity: Int?,
- @JsonProperty("num_list_users") val num_list_users: Int?,
- @JsonProperty("num_favorites") val num_favorites: Int?,
- @JsonProperty("num_scoring_users") val num_scoring_users: Int?,
- @JsonProperty("start_season") val start_season: StartSeason?,
+ @JsonProperty("num_list_users") val numListUsers: Int?,
+ @JsonProperty("num_favorites") val numFavorites: Int?,
+ @JsonProperty("num_scoring_users") val numScoringUsers: Int?,
+ @JsonProperty("start_season") val startSeason: StartSeason?,
@JsonProperty("broadcast") val broadcast: Broadcast?,
@JsonProperty("nsfw") val nsfw: String?,
- @JsonProperty("created_at") val created_at: String?,
- @JsonProperty("updated_at") val updated_at: String?
+ @JsonProperty("created_at") val createdAt: String?,
+ @JsonProperty("updated_at") val updatedAt: String?
)
data class ListStatus(
@JsonProperty("status") val status: String?,
@JsonProperty("score") val score: Int,
- @JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
- @JsonProperty("is_rewatching") val is_rewatching: Boolean,
- @JsonProperty("updated_at") val updated_at: String,
+ @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
+ @JsonProperty("is_rewatching") val isRewatching: Boolean,
+ @JsonProperty("updated_at") val updatedAt: String,
)
data class Data(
@JsonProperty("node") val node: Node,
- @JsonProperty("list_status") val list_status: ListStatus?,
+ @JsonProperty("list_status") val listStatus: ListStatus?,
) {
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
this.node.title,
"https://myanimelist.net/anime/${this.node.id}/",
this.node.id.toString(),
- this.list_status?.num_episodes_watched,
- this.node.num_episodes,
- this.list_status?.score?.times(10),
- parseDateLong(this.list_status?.updated_at),
+ this.listStatus?.numEpisodesWatched,
+ this.node.numEpisodes,
+ this.listStatus?.score?.times(10),
+ parseDateLong(this.listStatus?.updatedAt),
"MAL",
TvType.Anime,
- this.node.main_picture?.large ?: this.node.main_picture?.medium,
+ this.node.mainPicture?.large ?: this.node.mainPicture?.medium,
null,
null,
plot = this.node.synopsis,
+ releaseDate = if (this.node.startDate == null) null else try {Date.from(
+ Instant.from(
+ DateTimeFormatter.ofPattern(if (this.node.startDate.length == 4) "yyyy" else if (this.node.startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd")
+ .parse(this.node.startDate)
+ )
+ )} catch (_: RuntimeException) {null}
)
}
}
@@ -470,8 +480,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
)
data class Broadcast(
- @JsonProperty("day_of_the_week") val day_of_the_week: String?,
- @JsonProperty("start_time") val start_time: String?
+ @JsonProperty("day_of_the_week") val dayOfTheWeek: String?,
+ @JsonProperty("start_time") val startTime: String?
)
private fun getMalAnimeListCached(): Array? {
@@ -491,14 +501,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
val list = getMalAnimeListSmart()?.groupBy {
- convertToStatus(it.list_status?.status ?: "").stringRes
+ convertToStatus(it.listStatus?.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.toLibraryItem() }
} ?: emptyMap()
// To fill empty lists when MAL does not return them
val baseMap =
- MalStatusType.values().filter { it.value >= 0 }.associate {
+ MalStatusType.entries.filter { it.value >= 0 }.associate {
it.stringRes to emptyList()
}
@@ -509,6 +519,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
+ ListSorting.ReleaseDateNew,
+ ListSorting.ReleaseDateOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
@@ -573,7 +585,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).text
val values = parseJson(res)
val titles =
- values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) }
+ values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) }
for (t in titles) {
allTitles[t.id] = t
}
@@ -582,11 +594,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
- fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
+ private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
// No time remaining if the show has already ended
try {
endDate?.let {
- if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
+ if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it)
+ ?.before(Date.from(Instant.now())) != false
+ ) return@convertJapanTimeToTimeRemaining null
}
} catch (e: ParseException) {
logError(e)
@@ -603,7 +617,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
val currentYear = currentDate.get(Calendar.YEAR)
- val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
+ val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault())
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
val parsedDate =
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
@@ -647,13 +661,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
id: Int,
status: MalStatusType? = null,
score: Int? = null,
- num_watched_episodes: Int? = null,
+ numWatchedEpisodes: Int? = null,
): Boolean {
val res = setScoreRequest(
id,
if (status == null) null else malStatusAsString[maxOf(0, status.value)],
score,
- num_watched_episodes
+ numWatchedEpisodes
)
return if (res.isNullOrBlank()) {
@@ -670,17 +684,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
+ @Suppress("UNCHECKED_CAST")
private suspend fun setScoreRequest(
id: Int,
status: String? = null,
score: Int? = null,
- num_watched_episodes: Int? = null,
+ numWatchedEpisodes: Int? = null,
): String? {
val data = mapOf(
"status" to status,
"score" to score?.toString(),
- "num_watched_episodes" to num_watched_episodes?.toString()
- ).filter { it.value != null } as Map
+ "num_watched_episodes" to numWatchedEpisodes?.toString()
+ ).filterValues { it != null } as Map
return app.put(
"$apiUrl/v2/anime/$id/my_list_status",
@@ -693,10 +708,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class ResponseToken(
- @JsonProperty("token_type") val token_type: String,
- @JsonProperty("expires_in") val expires_in: Int,
- @JsonProperty("access_token") val access_token: String,
- @JsonProperty("refresh_token") val refresh_token: String,
+ @JsonProperty("token_type") val tokenType: String,
+ @JsonProperty("expires_in") val expiresIn: Int,
+ @JsonProperty("access_token") val accessToken: String,
+ @JsonProperty("refresh_token") val refreshToken: String,
)
data class MalRoot(
@@ -705,7 +720,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class MalDatum(
@JsonProperty("node") val node: MalNode,
- @JsonProperty("list_status") val list_status: MalStatus,
+ @JsonProperty("list_status") val listStatus: MalStatus,
)
data class MalNode(
@@ -722,16 +737,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class MalStatus(
@JsonProperty("status") val status: String,
@JsonProperty("score") val score: Int,
- @JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
- @JsonProperty("is_rewatching") val is_rewatching: Boolean,
- @JsonProperty("updated_at") val updated_at: String,
+ @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
+ @JsonProperty("is_rewatching") val isRewatching: Boolean,
+ @JsonProperty("updated_at") val updatedAt: String,
)
data class MalUser(
@JsonProperty("id") val id: Int,
@JsonProperty("name") val name: String,
@JsonProperty("location") val location: String,
- @JsonProperty("joined_at") val joined_at: String,
+ @JsonProperty("joined_at") val joinedAt: String,
@JsonProperty("picture") val picture: String?,
)
@@ -744,9 +759,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class SmallMalAnime(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String?,
- @JsonProperty("num_episodes") val num_episodes: Int,
- @JsonProperty("my_list_status") val my_list_status: MalStatus?,
- @JsonProperty("main_picture") val main_picture: MalMainPicture?,
+ @JsonProperty("num_episodes") val numEpisodes: Int,
+ @JsonProperty("my_list_status") val myListStatus: MalStatus?,
+ @JsonProperty("main_picture") val mainPicture: MalMainPicture?,
)
data class MalSearchNode(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
index 6412ff1b79b..37b95614661 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
@@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
-import com.lagradost.cloudstream3.utils.AppContextUtils
import com.lagradost.cloudstream3.utils.AppUtils
import okhttp3.Interceptor
import okhttp3.Response
@@ -30,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
companion object {
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
- const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
- const val host = "https://api.opensubtitles.com/api/v1"
+ const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
+ const val HOST = "https://api.opensubtitles.com/api/v1"
const val TAG = "OPENSUBS"
- const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms
+ const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms
var currentCoolDown: Long = 0L
var currentSession: SubtitleOAuthEntity? = null
}
@@ -49,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
chain.request().newBuilder()
.removeHeader("user-agent")
.addHeader("user-agent", userAgent)
- .addHeader("Api-Key", apiKey)
+ .addHeader("Api-Key", API_KEY)
.build()
)
}
@@ -66,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
}
private fun throwGotTooManyRequests() {
- currentCoolDown = unixTimeMs + coolDownDuration
+ currentCoolDown = unixTimeMs + COOLDOWN_DURATION
throw ErrorLoadingException("Too many requests")
}
@@ -115,7 +114,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
private suspend fun initLogin(username: String, password: String): Boolean {
//Log.i(TAG, "DATA = [$username] [$password]")
val response = app.post(
- url = "$host/login",
+ url = "$HOST/login",
headers = mapOf(
"Content-Type" to "application/json",
),
@@ -134,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
SubtitleOAuthEntity(
user = username,
pass = password,
- access_token = token.token ?: run {
+ accessToken = token.token ?: run {
return false
})
)
@@ -197,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val searchQueryUrl = when (imdbId > 0) {
//Use imdb_id to search if its valid
- true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
- false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
+ true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
+ false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
}
val req = app.get(
@@ -233,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
val year = featureDetails?.year ?: query.year
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
- val isHearingImpaired = attr.hearing_impaired ?: false
+ val isHearingImpaired = attr.hearingImpaired ?: false
//Log.i(TAG, "Result id/name => ${item.id} / $name")
item.attributes?.files?.forEach { file ->
val resultData = file.fileId?.toString() ?: ""
@@ -266,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
throwIfCantDoRequest()
val req = app.post(
- url = "$host/download",
+ url = "$HOST/download",
headers = mapOf(
Pair(
"Authorization",
- "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
+ "Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}"
),
Pair("Content-Type", "application/json"),
Pair("Accept", "*/*")
@@ -299,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
data class SubtitleOAuthEntity(
var user: String,
var pass: String,
- var access_token: String,
+ var accessToken: String,
)
data class OAuthToken(
@@ -324,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
@JsonProperty("url") var url: String? = null,
@JsonProperty("files") var files: List? = listOf(),
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(),
- @JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null,
+ @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null,
)
data class ResultFiles(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
index 27975d1902d..50517f9d1ac 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
@@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.toJson
+import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
import okhttp3.Interceptor
import okhttp3.Response
import java.math.BigInteger
@@ -38,6 +39,7 @@ import java.security.SecureRandom
import java.text.SimpleDateFormat
import java.time.Instant
import java.util.Date
+import java.util.Locale
import java.util.TimeZone
import kotlin.time.Duration
import kotlin.time.DurationUnit
@@ -144,8 +146,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
companion object {
- private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID
- private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET
+ private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID
+ private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET
private var lastLoginState = ""
const val SIMKL_TOKEN_KEY: String = "simkl_token"
@@ -154,10 +156,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time"
/** 2014-09-01T09:10:11Z -> 1409562611 */
- private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
+ private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"
fun getUnixTime(string: String?): Long? {
return try {
- SimpleDateFormat(simklDateFormat).apply {
+ SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
this.timeZone = TimeZone.getTimeZone("UTC")
}.parse(
string ?: return null
@@ -171,7 +173,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
/** 1409562611 -> 2014-09-01T09:10:11Z */
fun getDateTime(unixTime: Long?): String? {
return try {
- SimpleDateFormat(simklDateFormat).apply {
+ SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
this.timeZone = TimeZone.getTimeZone("UTC")
}.format(
Date.from(
@@ -208,7 +210,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
companion object {
fun fromString(string: String): SimklListStatusType? {
- return SimklListStatusType.values().firstOrNull {
+ return SimklListStatusType.entries.firstOrNull {
it.originalName == string
}
}
@@ -219,17 +221,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
data class TokenRequest(
@JsonProperty("code") val code: String,
- @JsonProperty("client_id") val client_id: String = clientId,
- @JsonProperty("client_secret") val client_secret: String = clientSecret,
- @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl",
- @JsonProperty("grant_type") val grant_type: String = "authorization_code"
+ @JsonProperty("client_id") val clientId: String = CLIENT_ID,
+ @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET,
+ @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl",
+ @JsonProperty("grant_type") val grantType: String = "authorization_code"
)
data class TokenResponse(
/** No expiration date */
- val access_token: String,
- val token_type: String,
- val scope: String
+ @JsonProperty("access_token") val accessToken: String,
+ @JsonProperty("token_type") val tokenType: String,
+ @JsonProperty("scope") val scope: String
)
// -------------------
@@ -261,15 +263,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
// -------------------
data class ActivitiesResponse(
- val all: String?,
- val tv_shows: UpdatedAt,
- val anime: UpdatedAt,
- val movies: UpdatedAt,
+ @JsonProperty("all") val all: String?,
+ @JsonProperty("tv_shows") val tvShows: UpdatedAt,
+ @JsonProperty("anime") val anime: UpdatedAt,
+ @JsonProperty("movies") val movies: UpdatedAt,
) {
data class UpdatedAt(
- val all: String?,
- val removed_from_list: String?,
- val rated_at: String?,
+ @JsonProperty("all") val all: String?,
+ @JsonProperty("removed_from_list") val removedFromList: String?,
+ @JsonProperty("rated_at") val ratedAt: String?,
)
}
@@ -308,7 +310,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("title") val title: String?,
@JsonProperty("year") val year: Int?,
@JsonProperty("ids") val ids: Ids?,
- @JsonProperty("total_episodes") val total_episodes: Int? = null,
+ @JsonProperty("total_episodes") val totalEpisodes: Int? = null,
@JsonProperty("status") val status: String? = null,
@JsonProperty("poster") val poster: String? = null,
@JsonProperty("type") val type: String? = null,
@@ -540,7 +542,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
debugPrint { "Requesting episodes from $url" }
- return app.get(url, params = mapOf("client_id" to clientId))
+ return app.get(url, params = mapOf("client_id" to CLIENT_ID))
.parsedSafe>()?.also {
val cacheTime =
if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
@@ -558,7 +560,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("seasons") seasons: List? = null,
@JsonProperty("episodes") episodes: List? = null,
@JsonProperty("rating") val rating: Int? = null,
- @JsonProperty("rated_at") val rated_at: String? = null,
+ @JsonProperty("rated_at") val ratedAt: String? = null,
) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@@ -567,7 +569,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?,
@JsonProperty("rating") val rating: Int,
- @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime)
+ @JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime)
) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@@ -576,7 +578,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?,
@JsonProperty("to") val to: String,
- @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime)
+ @JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime)
) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@@ -631,24 +633,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
interface Metadata {
- val last_watched_at: String?
+ val lastWatchedAt: String?
val status: String?
- val user_rating: Int?
- val last_watched: String?
- val watched_episodes_count: Int?
- val total_episodes_count: Int?
+ val userRating: Int?
+ val lastWatched: String?
+ val watchedEpisodesCount: Int?
+ val totalEpisodesCount: Int?
fun getIds(): ShowMetadata.Show.Ids
fun toLibraryItem(): SyncAPI.LibraryItem
}
data class MovieMetadata(
- override val last_watched_at: String?,
- override val status: String,
- override val user_rating: Int?,
- override val last_watched: String?,
- override val watched_episodes_count: Int?,
- override val total_episodes_count: Int?,
+ @JsonProperty("last_watched_at") override val lastWatchedAt: String?,
+ @JsonProperty("status") override val status: String,
+ @JsonProperty("user_rating") override val userRating: Int?,
+ @JsonProperty("last_watched") override val lastWatched: String?,
+ @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?,
+ @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?,
val movie: ShowMetadata.Show
) : Metadata {
override fun getIds(): ShowMetadata.Show.Ids {
@@ -660,27 +662,28 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
this.movie.title,
"https://simkl.com/tv/${movie.ids.simkl}",
movie.ids.simkl.toString(),
- this.watched_episodes_count,
- this.total_episodes_count,
- this.user_rating?.times(10),
- getUnixTime(last_watched_at) ?: 0,
+ this.watchedEpisodesCount,
+ this.totalEpisodesCount,
+ this.userRating?.times(10),
+ getUnixTime(lastWatchedAt) ?: 0,
"Simkl",
TvType.Movie,
this.movie.poster?.let { getPosterUrl(it) },
null,
null,
- movie.ids.simkl,
+ this.movie.year?.toYear(),
+ movie.ids.simkl
)
}
}
data class ShowMetadata(
- @JsonProperty("last_watched_at") override val last_watched_at: String?,
+ @JsonProperty("last_watched_at") override val lastWatchedAt: String?,
@JsonProperty("status") override val status: String,
- @JsonProperty("user_rating") override val user_rating: Int?,
- @JsonProperty("last_watched") override val last_watched: String?,
- @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?,
- @JsonProperty("total_episodes_count") override val total_episodes_count: Int?,
+ @JsonProperty("user_rating") override val userRating: Int?,
+ @JsonProperty("last_watched") override val lastWatched: String?,
+ @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?,
+ @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?,
@JsonProperty("show") val show: Show
) : Metadata {
override fun getIds(): Show.Ids {
@@ -692,15 +695,16 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
this.show.title,
"https://simkl.com/tv/${show.ids.simkl}",
show.ids.simkl.toString(),
- this.watched_episodes_count,
- this.total_episodes_count,
- this.user_rating?.times(10),
- getUnixTime(last_watched_at) ?: 0,
+ this.watchedEpisodesCount,
+ this.totalEpisodesCount,
+ this.userRating?.times(10),
+ getUnixTime(lastWatchedAt) ?: 0,
"Simkl",
TvType.Anime,
this.show.poster?.let { getPosterUrl(it) },
null,
null,
+ this.show.year?.toYear(),
show.ids.simkl
)
}
@@ -749,7 +753,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
chain.request()
.newBuilder()
.addHeader("Authorization", "Bearer $token")
- .addHeader("simkl-api-key", clientId)
+ .addHeader("simkl-api-key", CLIENT_ID)
.build()
)
}
@@ -810,7 +814,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val episodeConstructor = SimklEpisodeConstructor(
searchResult.ids?.simkl,
searchResult.type,
- searchResult.total_episodes,
+ searchResult.totalEpisodes,
searchResult.hasEnded()
)
@@ -832,12 +836,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
)
}
?: return null,
- score = foundItem.user_rating,
- watchedEpisodes = foundItem.watched_episodes_count,
- maxEpisodes = searchResult.total_episodes,
+ score = foundItem.userRating,
+ watchedEpisodes = foundItem.watchedEpisodesCount,
+ maxEpisodes = searchResult.totalEpisodes,
episodeConstructor = episodeConstructor,
- oldEpisodes = foundItem.watched_episodes_count ?: 0,
- oldScore = foundItem.user_rating,
+ oldEpisodes = foundItem.watchedEpisodesCount ?: 0,
+ oldScore = foundItem.userRating,
oldStatus = foundItem.status
)
} else {
@@ -845,7 +849,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
status = SyncWatchType.fromInternalId(SimklListStatusType.None.value),
score = 0,
watchedEpisodes = 0,
- maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes,
+ maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes,
episodeConstructor = episodeConstructor,
oldEpisodes = 0,
oldStatus = null,
@@ -891,12 +895,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
/** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */
- suspend fun searchByIds(serviceMap: Map): Array? {
+ private suspend fun searchByIds(serviceMap: Map): Array? {
if (serviceMap.isEmpty()) return emptyArray()
return app.get(
"$mainUrl/search/id",
- params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) ->
+ params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) ->
service.originalName to id
}
).parsedSafe()
@@ -904,14 +908,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun search(name: String): List? {
return app.get(
- "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name)
+ "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name)
).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() }
}
override fun authenticate(activity: FragmentActivity?) {
lastLoginState = BigInteger(130, SecureRandom()).toString(32)
val url =
- "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState"
+ "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}&state=$lastLoginState"
openBrowser(url, activity)
}
@@ -961,15 +965,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val activities = getActivities()
val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME)
val lastRemoval = listOf(
- activities?.tv_shows?.removed_from_list,
- activities?.anime?.removed_from_list,
- activities?.movies?.removed_from_list
+ activities?.tvShows?.removedFromList,
+ activities?.anime?.removedFromList,
+ activities?.movies?.removedFromList
).maxOf {
getUnixTime(it) ?: -1
}
val lastRealUpdate =
listOf(
- activities?.tv_shows?.all,
+ activities?.tvShows?.all,
activities?.anime?.all,
activities?.movies?.all,
).maxOf {
@@ -1026,6 +1030,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
+ ListSorting.ReleaseDateNew,
+ ListSorting.ReleaseDateOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
@@ -1039,7 +1045,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getDevicePin(): OAuth2API.PinAuthData? {
val pinAuthResp = app.get(
- "$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}"
+ "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}"
).parsedSafe() ?: return null
return OAuth2API.PinAuthData(
@@ -1053,7 +1059,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean {
val pinAuthResp = app.get(
- "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId"
+ "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$CLIENT_ID"
).parsedSafe() ?: return false
if (pinAuthResp.accessToken != null) {
@@ -1088,7 +1094,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
).parsedSafe() ?: return false
switchToNewAccount()
- setKey(accountId, SIMKL_TOKEN_KEY, token.access_token)
+ setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken)
val user = getUser()
if (user == null) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt
index 0e233ece8da..8dad1f88cfe 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt
@@ -59,6 +59,7 @@ class SubSourceApi : AbstractSubProvider {
it?.subs?.filter { sub ->
sub.releaseName!!.contains(
String.format(
+ null,
"E%02d",
query.epNumber
)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
index a075cc2e3ba..9150cfc5e0c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
@@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) {
private val cache = threadSafeListOf()
private var cacheIndex: Int = 0
- const val cacheSize = 20
+ const val CACHE_SIZE = 20
}
private fun afterPluginsLoaded(forceReload: Boolean) {
@@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) {
val add = SavedLoadResponse(unixTime, response, lookingForHash)
synchronized(cache) {
- if (cache.size > cacheSize) {
+ if (cache.size > CACHE_SIZE) {
cache[cacheIndex] = add // rolling cache
- cacheIndex = (cacheIndex + 1) % cacheSize
+ cacheIndex = (cacheIndex + 1) % CACHE_SIZE
} else {
cache.add(add)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
index d90177f5069..e930961c550 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
@@ -112,6 +112,7 @@ abstract class BaseAdapter<
holder.onViewDetachedFromWindow()
}
+ @Suppress("UNCHECKED_CAST")
fun save(recyclerView: RecyclerView) {
for (child in recyclerView.children) {
val holder =
@@ -124,6 +125,7 @@ abstract class BaseAdapter<
stateViewModel.layoutManagerStates[id]?.clear()
}
+ @Suppress("UNCHECKED_CAST")
private fun getState(holder: ViewHolderState): S? =
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt
index 6bafa975f49..1eaac5056ef 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt
@@ -6,6 +6,7 @@ import android.view.Menu
import android.view.View.*
import android.widget.*
import androidx.appcompat.app.AlertDialog
+import androidx.media3.common.util.UnstableApi
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
@@ -263,6 +264,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
var isLoadingMore = false
+
override fun onMediaStatusUpdated() {
super.onMediaStatusUpdated()
val meta = getCurrentMetaData()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt
index 1a9549e175e..78ad2a6bfcb 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt
@@ -8,8 +8,8 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
-class GrdLayoutManager(val context: Context, _spanCount: Int) :
- GridLayoutManager(context, _spanCount) {
+class GrdLayoutManager(val context: Context, spanCount: Int) :
+ GridLayoutManager(context, spanCount) {
override fun onFocusSearchFailed(
focused: View,
focusDirection: Int,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt
index c70417763d0..4879d2e0b8e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt
@@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() {
FrameLayout.LayoutParams.WRAP_CONTENT)
binding.frame.addView(newStar)
- newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX
+ newStar.scaleX += Math.random().toFloat() * 1.5f
newStar.scaleY = newStar.scaleX
starW *= newStar.scaleX
starH *= newStar.scaleY
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt
index f721401e7d2..12a5ae2a29e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt
@@ -15,7 +15,7 @@ open class NonFinalAdapterListUpdateCallback
/**
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
*
- * @param adapter The Adapter to send updates to.
+ * @param mAdapter The Adapter to send updates to.
*/(private var mAdapter: RecyclerView.Adapter<*>) :
ListUpdateCallback {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt
index 9532d1a9b91..b778ba5a754 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt
@@ -13,7 +13,7 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab
NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24);
companion object {
- fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
+ fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE
}
}
@@ -36,6 +36,6 @@ enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @Dr
REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24);
companion object {
- fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
+ fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt
index 15e66b38f3e..5e2b97e57d5 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt
@@ -8,8 +8,10 @@ import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
+import androidx.annotation.OptIn
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
+import androidx.media3.common.util.UnstableApi
import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.USER_AGENT
@@ -29,6 +31,7 @@ class WebviewFragment : Fragment() {
}
binding?.webView?.webViewClient = object : WebViewClient() {
+ @OptIn(UnstableApi::class)
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
index 9a0263342b2..d211cb87ce6 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
@@ -1,9 +1,9 @@
package com.lagradost.cloudstream3.ui.download
-import android.annotation.SuppressLint
import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater
import android.view.ViewGroup
+import android.widget.CheckBox
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
@@ -31,47 +31,30 @@ const val DOWNLOAD_ACTION_LONG_CLICK = 5
const val DOWNLOAD_ACTION_GO_TO_CHILD = 0
const val DOWNLOAD_ACTION_LOAD_RESULT = 1
-abstract class VisualDownloadCached(
- open val currentBytes: Long,
- open val totalBytes: Long,
- open val data: VideoDownloadHelper.DownloadCached
-) {
-
- // Just to be extra-safe with areContentsTheSame
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is VisualDownloadCached) return false
-
- if (currentBytes != other.currentBytes) return false
- if (totalBytes != other.totalBytes) return false
- if (data != other.data) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = currentBytes.hashCode()
- result = 31 * result + totalBytes.hashCode()
- result = 31 * result + data.hashCode()
- return result
- }
+sealed class VisualDownloadCached {
+ abstract val currentBytes: Long
+ abstract val totalBytes: Long
+ abstract val data: VideoDownloadHelper.DownloadCached
+ abstract var isSelected: Boolean
+
+ data class Child(
+ override val currentBytes: Long,
+ override val totalBytes: Long,
+ override val data: VideoDownloadHelper.DownloadEpisodeCached,
+ override var isSelected: Boolean,
+ ) : VisualDownloadCached()
+
+ data class Header(
+ override val currentBytes: Long,
+ override val totalBytes: Long,
+ override val data: VideoDownloadHelper.DownloadHeaderCached,
+ override var isSelected: Boolean,
+ val child: VideoDownloadHelper.DownloadEpisodeCached?,
+ val currentOngoingDownloads: Int,
+ val totalDownloads: Int,
+ ) : VisualDownloadCached()
}
-data class VisualDownloadChildCached(
- override val currentBytes: Long,
- override val totalBytes: Long,
- override val data: VideoDownloadHelper.DownloadEpisodeCached,
-): VisualDownloadCached(currentBytes, totalBytes, data)
-
-data class VisualDownloadHeaderCached(
- override val currentBytes: Long,
- override val totalBytes: Long,
- override val data: VideoDownloadHelper.DownloadHeaderCached,
- val child: VideoDownloadHelper.DownloadEpisodeCached?,
- val currentOngoingDownloads: Int,
- val totalDownloads: Int,
-): VisualDownloadCached(currentBytes, totalBytes, data)
-
data class DownloadClickEvent(
val action: Int,
val data: VideoDownloadHelper.DownloadEpisodeCached
@@ -83,108 +66,191 @@ data class DownloadHeaderClickEvent(
)
class DownloadAdapter(
- private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
- private val mediaClickCallback: (DownloadClickEvent) -> Unit,
+ private val onHeaderClickEvent: (DownloadHeaderClickEvent) -> Unit,
+ private val onItemClickEvent: (DownloadClickEvent) -> Unit,
+ private val onItemSelectionChanged: (Int, Boolean) -> Unit,
) : ListAdapter(DiffCallback()) {
+ private var isMultiDeleteState: Boolean = false
+
companion object {
private const val VIEW_TYPE_HEADER = 0
private const val VIEW_TYPE_CHILD = 1
}
inner class DownloadViewHolder(
- private val binding: ViewBinding,
- private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
- private val mediaClickCallback: (DownloadClickEvent) -> Unit,
+ private val binding: ViewBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(card: VisualDownloadCached?) {
when (binding) {
- is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadHeaderCached)
- is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadChildCached)
+ is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadCached.Header)
+ is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadCached.Child)
}
}
- @SuppressLint("SetTextI18n")
- private fun bindHeader(card: VisualDownloadHeaderCached?) {
- if (binding !is DownloadHeaderEpisodeBinding) return
- card ?: return
- val d = card.data
+ private fun bindHeader(card: VisualDownloadCached.Header?) {
+ if (binding !is DownloadHeaderEpisodeBinding || card == null) return
+ val data = card.data
binding.apply {
- downloadHeaderPoster.apply {
- setImage(d.poster)
- setOnClickListener {
- clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_LOAD_RESULT, d))
+ episodeHolder.apply {
+ if (isMultiDeleteState) {
+ setOnClickListener {
+ toggleIsChecked(deleteCheckbox, data.id)
+ }
}
- }
- downloadHeaderTitle.text = d.name
- val formattedSizeString = formatShortFileSize(itemView.context, card.totalBytes)
+ setOnLongClickListener {
+ toggleIsChecked(deleteCheckbox, data.id)
+ true
+ }
+ }
- if (card.child != null) {
- downloadHeaderGotoChild.isVisible = false
-
- val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes)
- if (status == DownloadStatusTell.IsDone) {
- // We do this here instead if we are finished downloading
- // so that we can use the value from the view model
- // rather than extra unneeded disk operations and to prevent a
- // delay in updating download icon state.
- downloadButton.setProgress(card.currentBytes, card.totalBytes)
- downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes)
- // We will let the view model handle this
- downloadButton.doSetProgress = false
- downloadButton.progressBar.progressDrawable =
- downloadButton.getDrawableFromStatus(status)
- ?.let { ContextCompat.getDrawable(downloadButton.context, it) }
- downloadHeaderInfo.text = formattedSizeString
+ downloadHeaderPoster.apply {
+ setImage(data.poster)
+ if (isMultiDeleteState) {
+ setOnClickListener {
+ toggleIsChecked(deleteCheckbox, data.id)
+ }
} else {
- downloadButton.doSetProgress = true
- downloadButton.progressBar.progressDrawable =
- ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable)
+ setOnClickListener {
+ onHeaderClickEvent.invoke(
+ DownloadHeaderClickEvent(
+ DOWNLOAD_ACTION_LOAD_RESULT,
+ data
+ )
+ )
+ }
}
- downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback)
- downloadButton.isVisible = true
-
- episodeHolder.setOnClickListener {
- mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child))
- }
- } else {
- downloadButton.isVisible = false
- downloadHeaderGotoChild.isVisible = true
-
- try {
- downloadHeaderInfo.text = downloadHeaderInfo.context.getString(R.string.extra_info_format)
- .format(
- card.totalDownloads,
- downloadHeaderInfo.context.resources.getQuantityString(
- R.plurals.episodes,
- card.totalDownloads
- ),
- formattedSizeString
- )
- } catch (e: Exception) {
- // You probably formatted incorrectly
- downloadHeaderInfo.text = "Error"
- logError(e)
+ setOnLongClickListener {
+ toggleIsChecked(deleteCheckbox, data.id)
+ true
}
+ }
+ downloadHeaderTitle.text = data.name
+ val formattedSize = formatShortFileSize(itemView.context, card.totalBytes)
- episodeHolder.setOnClickListener {
- clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_GO_TO_CHILD, d))
+ if (card.child != null) {
+ handleChildDownload(card, formattedSize)
+ } else handleParentDownload(card, formattedSize)
+
+ if (isMultiDeleteState) {
+ deleteCheckbox.setOnCheckedChangeListener { _, isChecked ->
+ onItemSelectionChanged.invoke(data.id, isChecked)
}
+ } else deleteCheckbox.setOnCheckedChangeListener(null)
+
+ deleteCheckbox.apply {
+ isVisible = isMultiDeleteState
+ isChecked = card.isSelected
}
}
}
- private fun bindChild(card: VisualDownloadChildCached?) {
- if (binding !is DownloadChildEpisodeBinding) return
- card ?: return
- val d = card.data
+ private fun DownloadHeaderEpisodeBinding.handleChildDownload(
+ card: VisualDownloadCached.Header,
+ formattedSize: String
+ ) {
+ card.child ?: return
+ downloadHeaderGotoChild.isVisible = false
+
+ val posDur = getViewPos(card.data.id)
+ downloadHeaderEpisodeProgress.apply {
+ isVisible = posDur != null
+ posDur?.let {
+ val visualPos = it.fixVisual()
+ max = (visualPos.duration / 1000).toInt()
+ progress = (visualPos.position / 1000).toInt()
+ }
+ }
+
+ val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes)
+ if (status == DownloadStatusTell.IsDone) {
+ // We do this here instead if we are finished downloading
+ // so that we can use the value from the view model
+ // rather than extra unneeded disk operations and to prevent a
+ // delay in updating download icon state.
+ downloadButton.setProgress(card.currentBytes, card.totalBytes)
+ downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes)
+ // We will let the view model handle this
+ downloadButton.doSetProgress = false
+ downloadButton.progressBar.progressDrawable =
+ downloadButton.getDrawableFromStatus(status)
+ ?.let { ContextCompat.getDrawable(downloadButton.context, it) }
+ downloadHeaderInfo.text = formattedSize
+ } else {
+ // We need to make sure we restore the correct progress
+ // when we refresh data in the adapter.
+ downloadButton.resetView()
+ val drawable = downloadButton.getDrawableFromStatus(status)?.let {
+ ContextCompat.getDrawable(downloadButton.context, it)
+ }
+ downloadButton.statusView.setImageDrawable(drawable)
+ downloadButton.progressBar.progressDrawable =
+ ContextCompat.getDrawable(
+ downloadButton.context,
+ downloadButton.progressDrawable
+ )
+ }
+ downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, onItemClickEvent)
+ downloadButton.isVisible = !isMultiDeleteState
+
+ if (!isMultiDeleteState) {
+ episodeHolder.setOnClickListener {
+ onItemClickEvent.invoke(
+ DownloadClickEvent(
+ DOWNLOAD_ACTION_PLAY_FILE,
+ card.child
+ )
+ )
+ }
+ }
+ }
+
+ private fun DownloadHeaderEpisodeBinding.handleParentDownload(
+ card: VisualDownloadCached.Header,
+ formattedSize: String
+ ) {
+ downloadButton.isVisible = false
+ downloadHeaderEpisodeProgress.isVisible = false
+ downloadHeaderGotoChild.isVisible = !isMultiDeleteState
+
+ try {
+ downloadHeaderInfo.text =
+ downloadHeaderInfo.context.getString(R.string.extra_info_format).format(
+ card.totalDownloads,
+ downloadHeaderInfo.context.resources.getQuantityString(
+ R.plurals.episodes,
+ card.totalDownloads
+ ),
+ formattedSize
+ )
+ } catch (e: Exception) {
+ downloadHeaderInfo.text = null
+ logError(e)
+ }
+
+ if (!isMultiDeleteState) {
+ episodeHolder.setOnClickListener {
+ onHeaderClickEvent.invoke(
+ DownloadHeaderClickEvent(
+ DOWNLOAD_ACTION_GO_TO_CHILD,
+ card.data
+ )
+ )
+ }
+ }
+ }
+
+ private fun bindChild(card: VisualDownloadCached.Child?) {
+ if (binding !is DownloadChildEpisodeBinding || card == null) return
+
+ val data = card.data
binding.apply {
- val posDur = getViewPos(d.id)
+ val posDur = getViewPos(data.id)
downloadChildEpisodeProgress.apply {
isVisible = posDur != null
posDur?.let {
@@ -194,36 +260,87 @@ class DownloadAdapter(
}
}
- val status = downloadButton.getStatus(d.id, card.currentBytes, card.totalBytes)
+ val status = downloadButton.getStatus(data.id, card.currentBytes, card.totalBytes)
if (status == DownloadStatusTell.IsDone) {
// We do this here instead if we are finished downloading
// so that we can use the value from the view model
// rather than extra unneeded disk operations and to prevent a
// delay in updating download icon state.
downloadButton.setProgress(card.currentBytes, card.totalBytes)
- downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes)
+ downloadButton.applyMetaData(data.id, card.currentBytes, card.totalBytes)
// We will let the view model handle this
downloadButton.doSetProgress = false
downloadButton.progressBar.progressDrawable =
downloadButton.getDrawableFromStatus(status)
?.let { ContextCompat.getDrawable(downloadButton.context, it) }
- downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes)
+ downloadChildEpisodeTextExtra.text =
+ formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes)
} else {
- downloadButton.doSetProgress = true
+ // We need to make sure we restore the correct progress
+ // when we refresh data in the adapter.
+ downloadButton.resetView()
+ val drawable = downloadButton.getDrawableFromStatus(status)?.let {
+ ContextCompat.getDrawable(downloadButton.context, it)
+ }
+ downloadButton.statusView.setImageDrawable(drawable)
downloadButton.progressBar.progressDrawable =
- ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable)
+ ContextCompat.getDrawable(
+ downloadButton.context,
+ downloadButton.progressDrawable
+ )
}
- downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback)
- downloadButton.isVisible = true
+ downloadButton.setDefaultClickListener(
+ data,
+ downloadChildEpisodeTextExtra,
+ onItemClickEvent
+ )
+ downloadButton.isVisible = !isMultiDeleteState
downloadChildEpisodeText.apply {
- text = context.getNameFull(d.name, d.episode, d.season)
+ text = context.getNameFull(data.name, data.episode, data.season)
isSelected = true // Needed for text repeating
}
downloadChildEpisodeHolder.setOnClickListener {
- mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
+ onItemClickEvent.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, data))
+ }
+
+ downloadChildEpisodeHolder.apply {
+ when {
+ isMultiDeleteState -> {
+ setOnClickListener {
+ toggleIsChecked(deleteCheckbox, data.id)
+ }
+ }
+
+ else -> {
+ setOnClickListener {
+ onItemClickEvent.invoke(
+ DownloadClickEvent(
+ DOWNLOAD_ACTION_PLAY_FILE,
+ data
+ )
+ )
+ }
+ }
+ }
+
+ setOnLongClickListener {
+ toggleIsChecked(deleteCheckbox, data.id)
+ true
+ }
+ }
+
+ if (isMultiDeleteState) {
+ deleteCheckbox.setOnCheckedChangeListener { _, isChecked ->
+ onItemSelectionChanged.invoke(data.id, isChecked)
+ }
+ } else deleteCheckbox.setOnCheckedChangeListener(null)
+
+ deleteCheckbox.apply {
+ isVisible = isMultiDeleteState
+ isChecked = card.isSelected
}
}
}
@@ -236,7 +353,7 @@ class DownloadAdapter(
VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false)
else -> throw IllegalArgumentException("Invalid view type")
}
- return DownloadViewHolder(binding, clickCallback, mediaClickCallback)
+ return DownloadViewHolder(binding)
}
override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) {
@@ -245,18 +362,52 @@ class DownloadAdapter(
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
- is VisualDownloadChildCached -> VIEW_TYPE_CHILD
- is VisualDownloadHeaderCached -> VIEW_TYPE_HEADER
+ is VisualDownloadCached.Child -> VIEW_TYPE_CHILD
+ is VisualDownloadCached.Header -> VIEW_TYPE_HEADER
else -> throw IllegalArgumentException("Invalid data type at position $position")
}
}
+ fun setIsMultiDeleteState(value: Boolean) {
+ if (isMultiDeleteState == value) return
+ isMultiDeleteState = value
+ notifyItemRangeChanged(0, itemCount)
+ }
+
+ fun notifyAllSelected() {
+ currentList.indices.forEach { index ->
+ if (!currentList[index].isSelected) {
+ notifyItemChanged(index)
+ }
+ }
+ }
+
+ fun notifySelectionStates() {
+ currentList.indices.forEach { index ->
+ if (currentList[index].isSelected) {
+ notifyItemChanged(index)
+ }
+ }
+ }
+
+ private fun toggleIsChecked(checkbox: CheckBox, itemId: Int) {
+ val isChecked = !checkbox.isChecked
+ checkbox.isChecked = isChecked
+ onItemSelectionChanged.invoke(itemId, isChecked)
+ }
+
class DiffCallback : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean {
+ override fun areItemsTheSame(
+ oldItem: VisualDownloadCached,
+ newItem: VisualDownloadCached
+ ): Boolean {
return oldItem.data.id == newItem.data.id
}
- override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean {
+ override fun areContentsTheSame(
+ oldItem: VisualDownloadCached,
+ newItem: VisualDownloadCached
+ ): Boolean {
return oldItem == newItem
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
index c8c40e292cd..494e82e55e6 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
@@ -1,11 +1,12 @@
package com.lagradost.cloudstream3.ui.download
import android.content.DialogInterface
-import android.widget.Toast
+import android.net.Uri
import androidx.appcompat.app.AlertDialog
+import com.google.android.material.snackbar.Snackbar
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.CommonActivity.activity
-import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator
@@ -13,10 +14,13 @@ import com.lagradost.cloudstream3.ui.player.ExtractorUri
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
+import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
+import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
+import kotlinx.coroutines.MainScope
object DownloadButtonSetup {
fun handleDownloadClick(click: DownloadClickEvent) {
@@ -29,9 +33,15 @@ object DownloadButtonSetup {
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
- VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id)
+ VideoDownloadManager.deleteFilesAndUpdateSettings(
+ ctx,
+ setOf(id),
+ MainScope()
+ )
}
+
DialogInterface.BUTTON_NEGATIVE -> {
+ // Do nothing on cancel
}
}
}
@@ -56,11 +66,13 @@ object DownloadButtonSetup {
}
}
}
+
DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> {
VideoDownloadManager.downloadEvent.invoke(
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause)
)
}
+
DOWNLOAD_ACTION_RESUME_DOWNLOAD -> {
activity?.let { ctx ->
if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) {
@@ -79,6 +91,7 @@ object DownloadButtonSetup {
}
}
}
+
DOWNLOAD_ACTION_LONG_CLICK -> {
activity?.let { act ->
val length =
@@ -88,64 +101,80 @@ object DownloadButtonSetup {
)?.fileLength
?: 0
if (length > 0) {
- showToast(R.string.delete, Toast.LENGTH_LONG)
- } else {
- showToast(R.string.download, Toast.LENGTH_LONG)
+ showSnackbar(
+ act,
+ R.string.offline_file,
+ Snackbar.LENGTH_LONG
+ )
}
}
}
+
DOWNLOAD_ACTION_PLAY_FILE -> {
activity?.let { act ->
- val info =
- VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
- act,
- click.data.id
- ) ?: return
- val keyInfo = getKey(
- VideoDownloadManager.KEY_DOWNLOAD_INFO,
- click.data.id.toString()
- ) ?: return
val parent = getKey(
DOWNLOAD_HEADER_CACHE,
click.data.parentId.toString()
) ?: return
+ val episodes = getKeys(DOWNLOAD_EPISODE_CACHE)
+ ?.mapNotNull {
+ getKey(it)
+ }
+ ?.filter { it.parentId == click.data.parentId }
+
+ val currentSeason = click.data.season ?: 0
+ val currentEpisode = click.data.episode
+
+ val items = mutableListOf()
+
+ // Make sure we only get this episode and episodes after it,
+ // and that we can go to the next season if we need to.
+ val allRelevantEpisodes = episodes
+ ?.sortedWith(
+ compareByDescending { it.id == click.data.id }
+ .thenBy { it.season ?: 0 }
+ .thenBy { it.episode }
+ )
+ ?.filter {
+ if (it.season == null) return@filter true
+ val isCurrentOrLaterInSeason = it.season == currentSeason && (it.episode >= currentEpisode || it.id == click.data.id)
+ val isInFutureSeasons = it.season > currentSeason
+
+ isCurrentOrLaterInSeason || isInFutureSeasons
+ }
+
+ allRelevantEpisodes?.forEach {
+ val keyInfo = getKey(
+ VideoDownloadManager.KEY_DOWNLOAD_INFO,
+ it.id.toString()
+ ) ?: return@forEach
+
+ items.add(
+ ExtractorUri(
+ // We just use a temporary placeholder for the URI,
+ // it will be updated in generateLinks().
+ // We just do this for performance since getting
+ // all paths at once can be quite expensive.
+ uri = Uri.EMPTY,
+ id = it.id,
+ parentId = it.parentId,
+ name = act.getString(R.string.downloaded_file),
+ season = it.season,
+ episode = it.episode,
+ headerName = parent.name,
+ tvType = parent.type,
+ basePath = keyInfo.basePath,
+ displayName = keyInfo.displayName,
+ relativePath = keyInfo.relativePath,
+ )
+ )
+ }
+
act.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
- DownloadFileGenerator(
- listOf(
- ExtractorUri(
- uri = info.path,
-
- id = click.data.id,
- parentId = click.data.parentId,
- name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName
- season = click.data.season,
- episode = click.data.episode,
- headerName = parent.name,
- tvType = parent.type,
-
- basePath = keyInfo.basePath,
- displayName = keyInfo.displayName,
- relativePath = keyInfo.relativePath,
- )
- )
- )
+ DownloadFileGenerator(items)
)
- //R.id.global_to_navigation_player, PlayerFragment.newInstance(
- // UriData(
- // info.path.toString(),
- // keyInfo.basePath,
- // keyInfo.relativePath,
- // keyInfo.displayName,
- // click.data.parentId,
- // click.data.id,
- // headerName ?: "null",
- // if (click.data.episode <= 0) null else click.data.episode,
- // click.data.season
- // ),
- // getViewPos(click.data.id)?.position ?: 0
- //)
)
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
index 03db948c428..09c48a04337 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
@@ -1,29 +1,33 @@
package com.lagradost.cloudstream3.ui.download
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
+import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
-import com.lagradost.cloudstream3.utils.Coroutines.main
-import com.lagradost.cloudstream3.utils.DataStore.getKey
-import com.lagradost.cloudstream3.utils.DataStore.getKeys
+import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
+import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
-import com.lagradost.cloudstream3.utils.VideoDownloadHelper
-import com.lagradost.cloudstream3.utils.VideoDownloadManager
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
class DownloadChildFragment : Fragment() {
+ private lateinit var downloadsViewModel: DownloadViewModel
+ private var binding: FragmentChildDownloadsBinding? = null
+
companion object {
fun newInstance(headerName: String, folder: String): Bundle {
return Bundle().apply {
@@ -34,61 +38,54 @@ class DownloadChildFragment : Fragment() {
}
override fun onDestroyView() {
- downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
- downloadDeleteEventListener = null
+ detachBackPressedCallback()
binding = null
super.onDestroyView()
}
- private var binding: FragmentChildDownloadsBinding? = null
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
+ downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
}
- private fun updateList(folder: String) = main {
- context?.let { ctx ->
- val data = withContext(Dispatchers.IO) { ctx.getKeys(folder) }
- val eps = withContext(Dispatchers.IO) {
- data.mapNotNull { key ->
- context?.getKey(key)
- }.mapNotNull {
- val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id)
- ?: return@mapNotNull null
- VisualDownloadChildCached(
- currentBytes = info.fileLength,
- totalBytes = info.totalBytes,
- data = it,
- )
- }
- }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 }
- if (eps.isEmpty()) {
- activity?.onBackPressedDispatcher?.onBackPressed()
- return@main
- }
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
- (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps)
+ /**
+ * We never want to retain multi-delete state
+ * when navigating to downloads. Setting this state
+ * immediately can sometimes result in the observer
+ * not being notified in time to update the UI.
+ *
+ * By posting to the main looper, we ensure that this
+ * operation is executed after the view has been fully created
+ * and all initializations are completed, allowing the
+ * observer to properly receive and handle the state change.
+ */
+ Handler(Looper.getMainLooper()).post {
+ downloadsViewModel.setIsMultiDeleteState(false)
}
- }
- private var downloadDeleteEventListener: ((Int) -> Unit)? = null
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
+ /**
+ * We have to make sure selected items are
+ * cleared here as well so we don't run in an
+ * inconsistent state where selected items do
+ * not match the multi delete state we are in.
+ */
+ downloadsViewModel.clearSelectedItems()
val folder = arguments?.getString("folder")
val name = arguments?.getString("name")
if (folder == null) {
- activity?.onBackPressedDispatcher?.onBackPressed() // TODO FIX
+ activity?.onBackPressedDispatcher?.onBackPressed()
return
}
- fixPaddingStatusbar(binding?.downloadChildRoot)
binding?.downloadChildToolbar?.apply {
title = name
@@ -101,13 +98,55 @@ class DownloadChildFragment : Fragment() {
setAppBarNoScrollFlagsOnTV()
}
+ binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV()
+
+ observe(downloadsViewModel.childCards) {
+ if (it.isEmpty()) {
+ activity?.onBackPressedDispatcher?.onBackPressed()
+ return@observe
+ }
+
+ (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(it)
+ }
+ observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState ->
+ val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter
+ adapter?.setIsMultiDeleteState(isMultiDeleteState)
+ binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState
+ if (!isMultiDeleteState) {
+ detachBackPressedCallback()
+ downloadsViewModel.clearSelectedItems()
+ binding?.downloadChildToolbar?.isVisible = true
+ }
+ }
+ observe(downloadsViewModel.selectedBytes) {
+ updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it)
+ }
+ observe(downloadsViewModel.selectedItemIds) {
+ handleSelectedChange(it)
+ updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L)
+
+ binding?.btnDelete?.isVisible = it.isNotEmpty()
+ binding?.selectItemsText?.isVisible = it.isEmpty()
+
+ val allSelected = downloadsViewModel.isAllSelected()
+ if (allSelected) {
+ binding?.btnToggleAll?.setText(R.string.deselect_all)
+ } else binding?.btnToggleAll?.setText(R.string.select_all)
+ }
+
val adapter = DownloadAdapter(
{},
- { downloadClickEvent ->
- handleDownloadClick(downloadClickEvent)
- if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
- setUpDownloadDeleteListener(folder)
- }
+ { click ->
+ if (click.action == DOWNLOAD_ACTION_DELETE_FILE) {
+ context?.let { ctx ->
+ downloadsViewModel.handleSingleDelete(ctx, click.data.id)
+ }
+ } else handleDownloadClick(click)
+ },
+ { itemId, isChecked ->
+ if (isChecked) {
+ downloadsViewModel.addSelected(itemId)
+ } else downloadsViewModel.removeSelected(itemId)
}
)
@@ -122,18 +161,47 @@ class DownloadChildFragment : Fragment() {
)
}
- updateList(folder)
+ context?.let { downloadsViewModel.updateChildList(it, folder) }
+ fixPaddingStatusbar(binding?.downloadChildRoot)
}
- private fun setUpDownloadDeleteListener(folder: String) {
- downloadDeleteEventListener = { id: Int ->
- val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList
- if (list != null) {
- if (list.any { it.data.id == id }) {
- updateList(folder)
+ private fun handleSelectedChange(selected: MutableSet) {
+ if (selected.isNotEmpty()) {
+ binding?.downloadDeleteAppbar?.isVisible = true
+ binding?.downloadChildToolbar?.isVisible = false
+ activity?.attachBackPressedCallback {
+ downloadsViewModel.setIsMultiDeleteState(false)
+ }
+
+ binding?.btnDelete?.setOnClickListener {
+ context?.let { ctx ->
+ downloadsViewModel.handleMultiDelete(ctx)
}
}
+
+ binding?.btnCancel?.setOnClickListener {
+ downloadsViewModel.setIsMultiDeleteState(false)
+ }
+
+ binding?.btnToggleAll?.setOnClickListener {
+ val allSelected = downloadsViewModel.isAllSelected()
+ val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter
+ if (allSelected) {
+ adapter?.notifySelectionStates()
+ downloadsViewModel.clearSelectedItems()
+ } else {
+ adapter?.notifyAllSelected()
+ downloadsViewModel.selectAllItems()
+ }
+ }
+
+ downloadsViewModel.setIsMultiDeleteState(true)
}
- downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
+ }
+
+ private fun updateDeleteButton(count: Int, selectedBytes: Long) {
+ val formattedSize = formatShortFileSize(context, selectedBytes)
+ binding?.btnDelete?.text =
+ getString(R.string.delete_format).format(count, formattedSize)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
index 23d546e1083..447b4f1310f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
@@ -8,6 +8,8 @@ import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.os.Build
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater
import android.view.View
@@ -17,7 +19,6 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
@@ -27,7 +28,7 @@ import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
import com.lagradost.cloudstream3.databinding.StreamInputBinding
-import com.lagradost.cloudstream3.isMovieType
+import com.lagradost.cloudstream3.isEpisodeBased
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
@@ -40,20 +41,22 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
+import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
+import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
-import com.lagradost.cloudstream3.utils.DataStore
+import com.lagradost.cloudstream3.utils.DataStore.getFolderName
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
-import com.lagradost.cloudstream3.utils.VideoDownloadManager
import java.net.URI
const val DOWNLOAD_NAVIGATE_TO = "downloadpage"
class DownloadFragment : Fragment() {
private lateinit var downloadsViewModel: DownloadViewModel
+ private var binding: FragmentDownloadsBinding? = null
private fun View.setLayoutWidth(weight: Long) {
val param = LinearLayout.LayoutParams(
@@ -65,14 +68,11 @@ class DownloadFragment : Fragment() {
}
override fun onDestroyView() {
- downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
- downloadDeleteEventListener = null
+ detachBackPressedCallback()
binding = null
super.onDestroyView()
}
- private var binding: FragmentDownloadsBinding? = null
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -84,12 +84,34 @@ class DownloadFragment : Fragment() {
return localBinding.root
}
- private var downloadDeleteEventListener: ((Int) -> Unit)? = null
-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
hideKeyboard()
binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
+ binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV()
+
+ /**
+ * We never want to retain multi-delete state
+ * when navigating to downloads. Setting this state
+ * immediately can sometimes result in the observer
+ * not being notified in time to update the UI.
+ *
+ * By posting to the main looper, we ensure that this
+ * operation is executed after the view has been fully created
+ * and all initializations are completed, allowing the
+ * observer to properly receive and handle the state change.
+ */
+ Handler(Looper.getMainLooper()).post {
+ downloadsViewModel.setIsMultiDeleteState(false)
+ }
+
+ /**
+ * We have to make sure selected items are
+ * cleared here as well so we don't run in an
+ * inconsistent state where selected items do
+ * not match the multi delete state we are in.
+ */
+ downloadsViewModel.clearSelectedItems()
observe(downloadsViewModel.headerCards) {
(binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it)
@@ -97,25 +119,82 @@ class DownloadFragment : Fragment() {
binding?.textNoDownloads?.isVisible = it.isEmpty()
}
observe(downloadsViewModel.availableBytes) {
- updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree)
+ updateStorageInfo(
+ view.context,
+ it,
+ R.string.free_storage,
+ binding?.downloadFreeTxt,
+ binding?.downloadFree
+ )
}
observe(downloadsViewModel.usedBytes) {
- updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed)
- binding?.downloadStorageAppbar?.isVisible = it > 0
+ updateStorageInfo(
+ view.context,
+ it,
+ R.string.used_storage,
+ binding?.downloadUsedTxt,
+ binding?.downloadUsed
+ )
+
+ // Prevent race condition and make sure
+ // we don't display it early
+ if (
+ downloadsViewModel.isMultiDeleteState.value == null ||
+ downloadsViewModel.isMultiDeleteState.value == false
+ ) binding?.downloadStorageAppbar?.isVisible = it > 0
}
observe(downloadsViewModel.downloadBytes) {
- updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp)
+ updateStorageInfo(
+ view.context,
+ it,
+ R.string.app_storage,
+ binding?.downloadAppTxt,
+ binding?.downloadApp
+ )
+ }
+ observe(downloadsViewModel.selectedBytes) {
+ updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it)
+ }
+ observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState ->
+ val adapter = binding?.downloadList?.adapter as? DownloadAdapter
+ adapter?.setIsMultiDeleteState(isMultiDeleteState)
+ binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState
+ if (!isMultiDeleteState) {
+ detachBackPressedCallback()
+ downloadsViewModel.clearSelectedItems()
+ // Prevent race condition and make sure
+ // we don't display it early
+ if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) {
+ binding?.downloadStorageAppbar?.isVisible = true
+ }
+ }
+ }
+ observe(downloadsViewModel.selectedItemIds) {
+ handleSelectedChange(it)
+ updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L)
+
+ binding?.btnDelete?.isVisible = it.isNotEmpty()
+ binding?.selectItemsText?.isVisible = it.isEmpty()
+
+ val allSelected = downloadsViewModel.isAllSelected()
+ if (allSelected) {
+ binding?.btnToggleAll?.setText(R.string.deselect_all)
+ } else binding?.btnToggleAll?.setText(R.string.select_all)
}
val adapter = DownloadAdapter(
+ { click -> handleItemClick(click) },
{ click ->
- handleItemClick(click)
+ if (click.action == DOWNLOAD_ACTION_DELETE_FILE) {
+ context?.let { ctx ->
+ downloadsViewModel.handleSingleDelete(ctx, click.data.id)
+ }
+ } else handleDownloadClick(click)
},
- { downloadClickEvent ->
- handleDownloadClick(downloadClickEvent)
- if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
- setUpDownloadDeleteListener()
- }
+ { itemId, isChecked ->
+ if (isChecked) {
+ downloadsViewModel.addSelected(itemId)
+ } else downloadsViewModel.removeSelected(itemId)
}
)
@@ -126,7 +205,6 @@ class DownloadFragment : Fragment() {
setLinearListLayout(
isHorizontal = false,
nextRight = FOCUS_SELF,
- nextUp = FOCUS_SELF,
nextDown = FOCUS_SELF,
)
}
@@ -147,35 +225,68 @@ class DownloadFragment : Fragment() {
handleScroll(scrollY - oldScrollY)
}
}
- downloadsViewModel.updateList(requireContext())
+
+ context?.let { downloadsViewModel.updateHeaderList(it) }
fixPaddingStatusbar(binding?.downloadRoot)
}
private fun handleItemClick(click: DownloadHeaderClickEvent) {
when (click.action) {
DOWNLOAD_ACTION_GO_TO_CHILD -> {
- if (!click.data.type.isMovieType()) {
- val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString())
+ if (click.data.type.isEpisodeBased()) {
+ val folder =
+ getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString())
activity?.navigate(
R.id.action_navigation_downloads_to_navigation_download_child,
DownloadChildFragment.newInstance(click.data.name, folder)
)
}
}
+
DOWNLOAD_ACTION_LOAD_RESULT -> {
- (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName)
+ activity?.loadResult(click.data.url, click.data.apiName)
}
}
}
- private fun setUpDownloadDeleteListener() {
- downloadDeleteEventListener = { id ->
- val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList
- if (list?.any { it.data.id == id } == true) {
- context?.let { downloadsViewModel.updateList(it) }
+ private fun handleSelectedChange(selected: MutableSet) {
+ if (selected.isNotEmpty()) {
+ binding?.downloadDeleteAppbar?.isVisible = true
+ binding?.downloadStorageAppbar?.isVisible = false
+ activity?.attachBackPressedCallback {
+ downloadsViewModel.setIsMultiDeleteState(false)
+ }
+
+ binding?.btnDelete?.setOnClickListener {
+ context?.let { ctx ->
+ downloadsViewModel.handleMultiDelete(ctx)
+ }
+ }
+
+ binding?.btnCancel?.setOnClickListener {
+ downloadsViewModel.setIsMultiDeleteState(false)
+ }
+
+ binding?.btnToggleAll?.setOnClickListener {
+ val allSelected = downloadsViewModel.isAllSelected()
+ val adapter = binding?.downloadList?.adapter as? DownloadAdapter
+ if (allSelected) {
+ adapter?.notifySelectionStates()
+ downloadsViewModel.clearSelectedItems()
+ } else {
+ adapter?.notifyAllSelected()
+ downloadsViewModel.selectAllItems()
+ }
}
+
+ downloadsViewModel.setIsMultiDeleteState(true)
}
- downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
+ }
+
+ private fun updateDeleteButton(count: Int, selectedBytes: Long) {
+ val formattedSize = formatShortFileSize(context, selectedBytes)
+ binding?.btnDelete?.text =
+ getString(R.string.delete_format).format(count, formattedSize)
}
private fun updateStorageInfo(
@@ -185,7 +296,10 @@ class DownloadFragment : Fragment() {
textView: TextView?,
view: View?
) {
- textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes))
+ textView?.text = getString(R.string.storage_size_format).format(
+ getString(stringRes),
+ formatShortFileSize(context, bytes)
+ )
view?.setLayoutWidth(bytes)
}
@@ -218,7 +332,9 @@ class DownloadFragment : Fragment() {
if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding)
}
- (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy ->
+ (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(
+ 0
+ )?.text?.toString()?.let { copy ->
val fixedText = copy.trim()
binding.streamUrl.setText(fixedText)
activateSwitchOnHls(fixedText, binding)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
index 83d96592809..137f1355e25 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
@@ -1,122 +1,439 @@
package com.lagradost.cloudstream3.ui.download
import android.content.Context
+import android.content.DialogInterface
import android.os.Environment
import android.os.StatFs
+import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.lagradost.cloudstream3.isMovieType
+import com.lagradost.cloudstream3.R
+import com.lagradost.cloudstream3.isEpisodeBased
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
+import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
+import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DownloadViewModel : ViewModel() {
- private val _headerCards =
- MutableLiveData>().apply { listOf() }
- val headerCards: LiveData> = _headerCards
- private val _usedBytes = MutableLiveData()
- private val _availableBytes = MutableLiveData()
- private val _downloadBytes = MutableLiveData()
+ private val _headerCards = MutableLiveData>()
+ val headerCards: LiveData> = _headerCards
+
+ private val _childCards = MutableLiveData>()
+ val childCards: LiveData> = _childCards
+ private val _usedBytes = MutableLiveData()
val usedBytes: LiveData = _usedBytes
+
+ private val _availableBytes = MutableLiveData()
val availableBytes: LiveData = _availableBytes
+
+ private val _downloadBytes = MutableLiveData()
val downloadBytes: LiveData = _downloadBytes
- private var previousVisual: List? = null
+ private val _selectedBytes = MutableLiveData(0)
+ val selectedBytes: LiveData = _selectedBytes
+
+ private val _isMultiDeleteState = MutableLiveData(false)
+ val isMultiDeleteState: LiveData = _isMultiDeleteState
+
+ private val _selectedItemIds = MutableLiveData>(mutableSetOf())
+ val selectedItemIds: LiveData> = _selectedItemIds
+
+ private var previousVisual: List? = null
+
+ fun setIsMultiDeleteState(value: Boolean) {
+ _isMultiDeleteState.postValue(value)
+ }
+
+ fun addSelected(itemId: Int) {
+ updateSelectedItems { it.add(itemId) }
+ }
+
+ fun removeSelected(itemId: Int) {
+ updateSelectedItems { it.remove(itemId) }
+ }
+
+ fun selectAllItems() {
+ val items = headerCards.value.orEmpty() + childCards.value.orEmpty()
+ updateSelectedItems { it.addAll(items.map { item -> item.data.id }) }
+ }
+
+ fun clearSelectedItems() {
+ // We need this to be done immediately
+ // so we can't use postValue
+ _selectedItemIds.value = mutableSetOf()
+ updateSelectedItems { it.clear() }
+ }
+
+ fun isAllSelected(): Boolean {
+ val currentSelected = selectedItemIds.value ?: return false
+ val items = headerCards.value.orEmpty() + childCards.value.orEmpty()
+ return items.count() == currentSelected.count() && items.all { it.data.id in currentSelected }
+ }
+
+ private fun updateSelectedItems(action: (MutableSet) -> Unit) {
+ val currentSelected = selectedItemIds.value ?: mutableSetOf()
+ action(currentSelected)
+ _selectedItemIds.postValue(currentSelected)
+ updateSelectedBytes()
+ updateSelectedCards()
+ }
+
+ private fun updateSelectedBytes() = viewModelScope.launchSafe {
+ val selectedItemsList = getSelectedItemsData() ?: return@launchSafe
+ val totalSelectedBytes = selectedItemsList.sumOf { it.totalBytes }
+ _selectedBytes.postValue(totalSelectedBytes)
+ }
+
+ private fun updateSelectedCards() = viewModelScope.launchSafe {
+ val currentSelected = selectedItemIds.value ?: return@launchSafe
+
+ headerCards.value?.let { headers ->
+ headers.forEach { header ->
+ header.isSelected = header.data.id in currentSelected
+ }
+ _headerCards.postValue(headers)
+ }
- fun updateList(context: Context) = viewModelScope.launchSafe {
- val children = withContext(Dispatchers.IO) {
- context.getKeys(DOWNLOAD_EPISODE_CACHE)
+ childCards.value?.let { children ->
+ children.forEach { child ->
+ child.isSelected = child.data.id in currentSelected
+ }
+ _childCards.postValue(children)
+ }
+ }
+
+ fun updateHeaderList(context: Context) = viewModelScope.launchSafe {
+ val visual = withContext(Dispatchers.IO) {
+ val children = context.getKeys(DOWNLOAD_EPISODE_CACHE)
.mapNotNull { context.getKey(it) }
.distinctBy { it.id } // Remove duplicates
+
+ val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) =
+ calculateDownloadStats(context, children)
+
+ val cached = context.getKeys(DOWNLOAD_HEADER_CACHE)
+ .mapNotNull { context.getKey(it) }
+
+ createVisualDownloadList(
+ context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads
+ )
+ }
+
+ if (visual != previousVisual) {
+ previousVisual = visual
+ updateStorageStats(visual)
+ _headerCards.postValue(visual)
}
+ }
+ private fun calculateDownloadStats(
+ context: Context,
+ children: List
+ ): Triple
Во тек
Завршени
Статус
@@ -379,7 +380,7 @@
URL на складиштето
Не може да се инсталира новата верзија на апликацијата
Прикажи постери од Kitsu
- Дали сте сигурни дека сакате да излезете\?
+ Дали сте сигурни дека сакате да излезете?
Предизвикува проблеми ако е превисоко поставено на уреди со мал простор за складирање, како што е Android TV.
Заобиколете го блокирањето на необработени URL-адреси на github користејќи jsDelivr. Може да предизвика ажурирањата да се одложат за неколку дена.
Да
@@ -421,7 +422,7 @@
1000 ms
NSFW
/%d
- /\?\?
+ /??
hello@world.com
+30
VLC
@@ -624,4 +625,5 @@
Грешка при пристапот до таблата со исечоци, обидете се повторно.
Грешка при копирање, копирајте го logcat и контактирајте со поддршката за апликацијата.
Аудио книга
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index d97e666c166..1c2d855e5d8 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -122,7 +122,7 @@
നിർത്തുക
തുടരുക
സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക
-\nഉറപ്പാണോ\?
+\nഉറപ്പാണോ?
തുടരുന്നു
പൂർത്തിയായി
അവസ്ഥ
@@ -280,4 +280,5 @@
എഡ്ജ് തരം
ഔട്ട്ലൈൻ നിറം
പശ്ചാത്തല നിറം
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml
index dca98e53c88..00555e5e388 100644
--- a/app/src/main/res/values-ms/strings.xml
+++ b/app/src/main/res/values-ms/strings.xml
@@ -14,7 +14,7 @@
Memuat turun kemaskini aplikasi…
Tidak
Ya
- Adakah anda pasti anda ingin keluar\?
+ Adakah anda pasti anda ingin keluar?
Keluarkan daripada ditonton
Tanda sebagai sudah ditonton
Tunjukkan pop timbul yang dilangkau untuk pembukaan/pengakhiran
@@ -55,6 +55,5 @@
Kongsi
Tetapan
Tutup
- Ep
- cuba
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml
index b2c0356a87c..ee890fbbd05 100644
--- a/app/src/main/res/values-mt/strings.xml
+++ b/app/src/main/res/values-mt/strings.xml
@@ -123,4 +123,5 @@
Bookmarks
Neħħi
Falla t-tniżżil
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml
index 0ebe3c6b246..31e6ef276ad 100644
--- a/app/src/main/res/values-my/strings.xml
+++ b/app/src/main/res/values-my/strings.xml
@@ -496,7 +496,7 @@
ထည့်ပြီး %s
အဆင့်သတ်မှတ်ထားပြီး
%d / 10
- /\?\?
+ /??
%s ချိတ်ဆက်ပြီး
ပိတ်ပါ
ပုံမှန်
@@ -550,4 +550,5 @@
သင်နဂိုတည်းကသတ်မှတ်ပြီး
လိုက်ဘရီရွေးချယ်ရန်
ဖြင့်ဖွင့်မည်
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml
index 49cb6cfa91b..bff19dd3a98 100644
--- a/app/src/main/res/values-ne/strings.xml
+++ b/app/src/main/res/values-ne/strings.xml
@@ -128,4 +128,5 @@
प्लेयरको उपशीर्षकको सेटिङ
रिपोजिटरी को नाम र यूआरएल
कपी गरियो!
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index b685489bf83..4bef20cc289 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -189,7 +189,7 @@
-30
+30
Dit wordt zeker permanent verwijderd %s
-\nWeet u het zeker\?
+\nWeet u het zeker?
%dm
\nremaining
Voortdurende
@@ -320,7 +320,7 @@
Sync
gewaardeerd
%d / 10
- /\?\?
+ /??
/%d
%s geverifieerd
Kon niet inloggen op %s
@@ -493,7 +493,7 @@
Bekijk de crash info
Deze lijst is leeg. Probeer een andere.
Alfabetisch (A tot Z)
- Weet je zeker dat je wilt afsluiten\?
+ Weet je zeker dat je wilt afsluiten?
Bijgewerkt (Oud naar Nieuw)
Beoordeling ( Hoog naar Laag)
Erfenis
@@ -608,4 +608,5 @@
Link opnieuw geladen
Autoroteer
Roteer
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml
index 95c527f9a8c..33ebe1b512e 100644
--- a/app/src/main/res/values-nn/strings.xml
+++ b/app/src/main/res/values-nn/strings.xml
@@ -118,7 +118,7 @@
-30
+30
Dette vil slette %s permanent.
-\nEr du sikker på dette\?
+\nEr du sikker på dette?
%dm
\ngjenstår
Pågåande
@@ -195,4 +195,5 @@
Bilde i bilde
Fortsett å sjå
Prøv tilkopling på nytt…
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml
index 7b013653386..41117f1041d 100644
--- a/app/src/main/res/values-no/strings.xml
+++ b/app/src/main/res/values-no/strings.xml
@@ -142,7 +142,7 @@
Stopp
Gjenoppta
Dette vil slette %s
-\nEr du sikker\?
+\nEr du sikker?
Pågående
Fullført
Posisjon
@@ -232,7 +232,7 @@
Bytt konto
Vurdert
%d/10
- /\?\?
+ /??
/%d
Anbefalt
Last inn fra fil
@@ -425,7 +425,7 @@
Lastet inn sikkerhetkopifil
Oppdateringer og sikkerhetskopier
@string/ova
- Avslutt\?
+ Avslutt?
Sensurerbart
Alle %s er allerede nedlastet
Kunne ikke installere den nye versjonen av programmet
@@ -538,4 +538,5 @@
Bruk
Hjelp
Profilbakgrunn
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml
index bdc55780cbd..a9cff7edf7d 100644
--- a/app/src/main/res/values-or/strings.xml
+++ b/app/src/main/res/values-or/strings.xml
@@ -31,7 +31,7 @@
ବ୍ରାଉଜର୍ରେ ଚଲାଅ
ଉପଶୀର୍ଷକ ଡାଉନଲୋଡ୍ କରିବା
/%d
- /\?\?
+ /??
ଅଧ୍ୟାୟ %d ମୁକ୍ତିଲାଭ କଲା!
ସ୍ୱତଃ ଡାଉନଲୋଡ୍
ଲିଙ୍କ୍ଗୁଡ଼ିକୁ ପୁନଃଲୋଡ୍ କରିବା
@@ -159,4 +159,5 @@
କୌଣସି ତଥ୍ୟ ନାହିଁ
%1$s ଅ %2$d
ଆଦ୍ୟ ବାଦ୍ ଦିଅ
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml
index 9f76f423722..a43d7bcfe5f 100644
--- a/app/src/main/res/values-pl/array.xml
+++ b/app/src/main/res/values-pl/array.xml
@@ -256,6 +256,7 @@
- Szary
- Amoled
- Flashbang
+ - System
- Material You
@@ -263,6 +264,7 @@
- Black
- Amoled
- Light
+ - System
- Monet
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 8e940c6136a..fe99ad7de8b 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -184,7 +184,7 @@
-30
+30
Spowoduje to trwałe usunięcie %s
-\nCzy jesteś pewien\?
+\nCzy jesteś pewien?
%dm
\npozostało
Bieżący
@@ -441,7 +441,7 @@
Instalowanie aktualizacji aplikacji…
%dm
Oznacz jako obejrzane
- Czy na pewno chcesz wyjść\?
+ Czy na pewno chcesz wyjść?
Pobieranie aktualizacji aplikacji…
/%d
Obsada: %s
@@ -457,7 +457,7 @@
127.0.0.1
Tryb kompatybilności
https://example.com
- /\?\?
+ /??
Instalator pakietów
@string/home_play
witaj@poczta.pl
@@ -610,14 +610,14 @@
Błąd dostępu do schowka. Spróbuj ponownie.
skopiowano!
Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji.
- Wyłącz optymalizację akumulatora
+ Wyłącz optymalizację baterii
Nie można otworzyć informacji o aplikacji CloudStream.
Muzyka
Audiobook
OK
Multimedia
- Użycie akumulatora przez aplikację jest już ustawione na nieograniczone
- Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych.
+ Użycie baterii przez aplikację jest już ustawione na nieograniczone
+ Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do ustawień aplikacji. Tam przewiń do użycia baterii przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać baterię. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach .
Resetuj
Nadchodzące w %s
Odcinek %2$d sezonu %1$d wyjdzie za
@@ -635,4 +635,31 @@
Odrzuć
Otwórz repozytorium
Odwiedź %s na swoim smartfonie lub komputerze i wprowadź powyższy kod
-
+ hide_player_control_names_key
+ Odtwarzaj od początku
+ Usuń wtyczkę
+ Uwaga
+ Otwórz lokalne wideo
+ Obecnie nie ma żadnych pobrań.
+ Ukryj nazwy elementów sterujących odtwarzacza
+ Data wydania (od najstarszej do najnowszej)
+ Data wydania (od najnowszej do najstarszej)
+ Dostępne do oglądania offline
+ Zaznacz wszystkie
+ Zaznacz elementy do usunięcia
+ Odznacz wszystkie
+ Czy na pewno chcesz na stałe usunąć następujące elementy?
+\n
+\n%s
+ Usuniesz na stale wszystkie odcinki następującego serialu:
+\n
+\n%s
+ Czy na pewno chcesz na stałe usunąć wszystkie odcinki następującego serialu?
+\n
+\n%s
+ Usuń pliki
+ Czy na pewno chcesz na stałe usunąć następujące odcinki %1$s?
+\n
+\n%2$s
+ Usuń (%1$d | %2$s)
+
\ No newline at end of file
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index ce20a8afbf9..421746742cf 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -183,7 +183,7 @@
Pôr em Pausa
Retomar
Isto apagará %s permanentemente
-\nTem a certeza\?
+\nTem a certeza?
%dm
\nem falta
Em Curso
@@ -305,7 +305,7 @@
Sincronizar
Nota
%d / 10
- /\?\?
+ /??
/%d
%s autenticado
Falha em autenticar para %s
@@ -409,7 +409,7 @@
Anime
Player visível - Procurar valor
Instalando atualização do app…
- Você tem certeza que deseja sair\?
+ Você tem certeza que deseja sair?
Versão
Encerramento
Limpar histórico
@@ -617,8 +617,8 @@
Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais.
Reiniciar
Episódio %1$d Episódio %2$d vai ser lançado em
- Por vir em
Fcast
Escolha o dispositivo
Transmitir
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml
index 5de97c7da9c..378e3aaec1b 100644
--- a/app/src/main/res/values-qt/strings.xml
+++ b/app/src/main/res/values-qt/strings.xml
@@ -81,11 +81,11 @@
aaaghhaaahhuahooo oha aaaghh
ahhhaauugghh
oha ooh ouuhhh oooohhahhh ouuhhh
- haaahhh ahoouuh
+ aaaagggg aaahh
ohahaaaauugghh ahooo aaahhu
- aaaghh aaaghhohahooooo ouuhhh oouuh ooo-ahahahooo-ahah ohaaaaaghh
+ uuuuk oohh uuhh og eeek ag uuuuhhh aagg aaaagggg ek g uuuuk
aaaaaahhaaahhuoouuhaaaaa aahooo
- ohahhaaahhuoh ooo-ahahahhhooo-ahah aaaghha oooohhoouuh aaaghhaaaghhooo-ahah
+ ooogg ek ug uugg uk ooh oogg ug aaagg oohh og oooohh aaaahhhuuk oh oooogg
ahaauuhaaaaaooooo ahooo aauuh aaaghhaooo-ahah
aauuh
aauugghh ah
@@ -94,7 +94,7 @@
aaaaa ahhhahhohoouuhahoooaaaghh aahhhaaaaa
oouuhoooohh ahhooo-ahah
haa oohaauugghhooh oh
- aaahhuoouuhouuhhh aauugghhahaaaghhoohahhoooohhouuhhh
+ aaaagggoooogg uuuugg aak aah aaaahhh ooogg uuuuuukh aah ooh
aaaghh ahhhahoooooo-ahah aaaghh
ahhh ooo-ahahahhahh ooooo aauugghh oooohhahhhahoooaoooohhouuhhh aaahhu
aauuhoha
@@ -177,14 +177,14 @@
auuuha h a ahuhaaaa
uaoh uhu uahaaaaoo
uauhah u aao u oah
- h u ahahh aoou ha
+ aaaagggoh uuugg uuuuggg (WiFi)
haoooo aaoou uou ah
oahuouooaouoa ouuhh
o ouou uhauuuoaah h
ou aouhouo aaooao hh
hhauhohhuu au aaohu
uhuoh o a ohahuhohoa hah
- ua hu ouo o aoau hah ah
+ aaaagggog oooogggoh
ah huu oouhhau aoaoaaohoo ha
a ahu uoo uoahuo uo
uo u ohouao
@@ -213,13 +213,13 @@
ggaaaahhhhhhh gaauuuuuuuaaaau
aaauuuuggggguu
ooo aagg hhhh
- ooo aagg hhhh
+ aaaahhh aaaagg
uuuuhhhoouuooog ooaaahhhh
uuu ugggg
ooo guggg ooh
auuuooohaaaaagh
uuuuuuuh aaaoo o
- ooooooouuuua aa aaagh agh
+ uuuuggg uuh aaaagg
AAAAUUUOH
aoughoooaaaa
oooouuuh
@@ -227,7 +227,7 @@
auughooo
ooooooa aauoh
aaaaagh oouoo aaaaaaa
- aaaaaagh uuohuoh
+ oooogggg aah aaaaaakh
aaaaaauo agghhhhhhaoouu
uuuuuuuuh
ouaaahh
@@ -247,4 +247,418 @@
oooooh uuaagh
@string/home_play
oouuhhh ahhooo-ahah
-
+ hide_player_control_names_key
+ uuuugg aaaahh oogg
+ aagg uuuuggg og %1$dm %2$ds
+ aaaahhh ag
+ aahh aaaagg uuuugg uh uuuuhhhh aag uuuuhhh
+ aahh oh uuhh
+ aahh ooogg aahh
+ oogg
+ uuuuhhhoh ooh uuuuhhhag
+ eeeeeek %d ooooggg
+ aaaagggah uuugg uuuugg
+ aaak oooohh
+ uuuuggg aaaahhhaahh uuuuhhh aaaagg aag uuuhh
+ oooohhhoooohh ooooggg uuh oog aag uuuugggag eeeeeek aahh uuuhh uuuuggguuugg
+ aag uuuuk
+ eeeek
+ aaahh aaaahh ooohh uuk uuugg
+ ooohh uuuuhh aaaahhhooh
+ oooohhhg aaaahh
+ oh
+ ah
+ uuuuhh
+ 18+
+ aag %s ooooggg oooogggaag
+ aaaahh aagg
+ ug uuuuhhh aaahh og oooohhhuuh
+ oooohh oohh oooohh (Seconds)
+ aag
+ ooh aagg uuuuhh uugg uugg uuh uuuuhh ag uuuuhh
+ eek aaaahhh
+ uuuugg
+ aaaaggg
+ oog aag
+ uuk aaak ah ooooggguh uuk uuugg %d ah ooh oogg
+ ah uuuuhhhg uuuhh
+ aahh
+ uuuugggooh
+ aaaagg
+ uug
+ oooogg oooogg
+ oooohhhoogg uuh uh uuuhh aaaaggguh og ooooggg uug aagg ek aaaaggg oog aaahh aagg uuuugggooohh
+\n
+\nJoin uuh uuuuggg ag uuuuhh eeeeek
+ uuugg aaaagg
+ oogg uugg uh
+ aaagg
+ aaaaggg
+ uuuuhh
+ aaahh
+ uuuuhhhaaak aagg
+ aaaagg og ooogg aaaahhhaah
+ aaaagg aah ah ooogg
+ aaaahhhuh aaaagg uuuuhhh
+ aaaagggaaaahh uuuugggg uuuuggg
+ %1$d %2$s
+ aahh oogg uuuuhh
+ uuuuhh aaaaaakg
+ uuuugggk
+ oooohhhg uugg
+ oohh eek aaaagggooh
+ aag og
+ oooogg uuuuhhh
+ oooohh
+ 4K
+ oohh ah uuk aagg ag aah
+ uug uuuugggoog
+ uuuuggguug uugg
+ aaaahhhaah %1$d %2$s
+ aaahh uuuugggh
+ oooohh oooogggoog
+ uuuuhhhaagg
+ uuh
+ uuh aahh uugg oooogg ag aagg ug ooooggguh
+ ooooggg %d
+ aahh
+ oooogggk
+ eeek aag uug uuuuhh ooh aak uuuuggg ooh uuuuhhh ug g eeeek oog h uuuuhh oooogggh ag aahh oooohh aaaagg uh oog uuuugg uuuugggog uug oog uh uuh aaaagg uuuuuukg uug aah uuuuuuk uuuuuukg ak ooh uuuhh aaaagggk
+\n
+\nSource A: 3
+\nQuality B: 7
+\nWill uuhh k uuuuhhhk ooogg uuuuhhhh uk 10
+\n
+\nNOTE: ah uug oog ug 10 og oogg uug aaaahh uuhh uuuugggaaaahh oogg aaaahhh oogg uuhh aahh uh loaded!
+ uuuuhhhug
+ %s aaaaggg uuhh aaaagggug
+ oog
+ aaaahhh
+ aaaagg aaaagggh
+ aaaagg ug ag %s
+ aagg aaaahhh aaaahhhog ag aaaahhh
+ aah aaaaggg aaaaggg
+ oohh aaaagg
+ ooh oogg aaaahh uuhh oogg aah oooohh ag ooooggg
+ aaahh uug uuuhh aaaahh uuuuggg og uuuuhhhk proxy…
+ oooohhhooh
+ uuhh
+ 30
+ %d / 10
+ aag aagg ag oog aaaagggak ooh uuugg %d oh ooh ooohh
+ uug
+ aak
+ uuuuhh aaagg
+ aaaahhh oohh
+ ooohh
+ uugg oohh oh aaahh oog uuuugggoh ah oooohhh ooh
+ ooooggguuk uh %s
+ uuuuhhhag aaaagggug aaaak
+ aagg aaaaggg
+ oooogg
+ aah
+ oooogggag
+ 1000 ak
+ uuuhh aaaahhh ooh
+ uuuuuukg
+ aaaahh oooohhhaaak
+ ooogg%s on your smartphone or computer and enter the above code
+ Can\t aah uuh aaaagg oog code, uuh ooogg uuuuggguuuuggg
+ eeeeeekuug
+ uuuugggh oooohhhag
+ uuuuhh ak oooogggh oh aaaagg
+ uuuugggoh aaagg aaaahhh (Mobile Data)
+ aaaahh oooohh oohh aaaahh
+ ooogg
+ ooogg
+ aaaagggug ooohh
+ https://examplecom
+ aag
+ aaaahhh aag
+ eeek
+ oooohh oooogggaah
+ oooogg (Low ah High)
+ aagg aaaahhhh aaagg aaaaggg ak uuuuhh aaaaggg
+ eeek uuuuhh uh aah aaaaaak aah oog aaaahhh uuuuuukah aah oog oooogg aaaahh ug ooooggg oh ooh aaaaggg
+ uuuuggg
+ aaahh oooohh uuhh
+ ooogg uuuuhh aaaahh
+ uuugg
+ uuuugggg
+ aaaagggak
+ 127001
+ oooohh uuuugg
+ aahh %s
+ uuh aaaagggag
+ aahh oogg aaaagg ooh opening/ending
+ oooohhhuuk
+ ooohh
+ aagg
+ oh
+ uuuuuukoooohhhah
+ aaaahh (High ug Low)
+ aaaaaakuuugg (A uh Z)
+ aagg oogg
+ aaaagggg aaaaggguug ooohh
+ uuh ah aaaahhhag
+ aaaahh aagg uuuuhhhuh
+ uuugg uug oog %s
+ aaak uuuuhhh
+ oog
+ aaaagggaah
+ ooogg aagg
+ uuuugg aaaaggg ug aah uug aahh ug uuuuhhh oohh aah memory, oohh og oooohhh ug
+ aaaahhh
+ oooogggh
+ ooohh aaaaaakag
+ ooogg
+ aaaaggg aaaahhhuuhh %1$d %2$s…
+ uuuhh aaaagg
+ oh
+ uug
+ uugg
+ oooogggoh
+ uugg uuhh oooohhhoog
+ aaahh
+ uuugg g uug uuuuhh attempts, aah aaaagg uugg uuuhh aaaagg uuuuhhh aag uug ah uuh uuugg
+ oog
+ aaaaggg uug oooohhhuh uuuuk
+ https://examplecom/examplemp4
+ oohh uuugg ag aag player uuuugggg
+ aaaaggg (New ah Old)
+ oohh
+ aag
+ aaaagg uuuugg oooohhhk oohh oooogggah
+ aaaaggguuuugg uuhh oohh ooooggg eeeeeek aaaagggh
+ ooooggg uugg uugg uuuugg
+ oooohh ah aaaahhh oogg oogg aahh %s
+ %1$s %2$d%3$s
+ %1$d%2$d
+ ek oooogggh ooohh
+ uug aaahh
+ uuuuk uuugg ag oogg
+ aaahh aaahh uug ooohh aaagg
+ uuugg uuhh
+ uuuuuk uuuuhh
+ oooohh oohh uh uuuugggah uuuugggaag
+ uuuuhhhh oooohhhk
+ password123
+ aaaagggk
+ hello@worldcom
+ oooogggoohh
+ uuuugg uuuuggg
+ aag oooohhh
+ uug oooohhhh
+ aaaak %s
+ uuhh
+ aaahh
+ %s aaaagggaaaahh
+ ooogg uug aah oh oh %s
+ aaaagg soon…
+ aah
+ uh
+ ag
+ ag
+ aah
+ ek
+ uuuuhhhuuh aag aaahh
+ eeek oohh uuhh oooohh oog oooogggooh aaaahhh
+ aaaagggh oog uuuk ug ooohh aak aahh ag aag
+ Downloaded: %d
+ Disabled: %d
+ aag downloaded: %d
+ uuhh aaaagggag ooooggguuuhh
+ Warning: ooooggguuhh 3 uuhh aah aahh oog aaaagggaaaaggg aah aaahh aaaahhhoog aaaagggaag uuh uugg uug uuuuhhh uug eeeeeek aah them!
+ ooh oooohhhooh oogg oooogg ooh uuh uh g ooogg oh uuhh uug uuhh ooh aah uuuuggg ooooggg
+ ooooggg
+ aaaagggh uuuugg
+ ooh uuuhh aahh
+ uuh uuuuhhh
+ aah ooh uuugg
+ uuuuhhh
+ uuuuhh aagg oooohhh
+ uug ooh oohh ooh aahh ag exit?
+ ooh
+ uuuugggaagg aah update…
+ eeeeeekooh aag update…
+ uuugg aag uuuuhhh aag uuh aaaaaak ug aag aak
+ aaaahh
+ aah oohh og uuuuhhh oogg uuhh
+ uuhh ug
+ uuhh
+ uuugg
+ oooogg
+ aaaahh
+ oooohh uuugg aagg uuuugg
+ eeeeek uuugg
+ aah aaaagggg
+ aaaaggg
+ aaaagg
+ eeeeek uuuuhhhuh
+ oooohhhuuh aag found, aaagg uug oog uuh oog aah
+ oooogggag
+ aaaahhhuugg
+ uuh uuhh uuuuggg uuuhh
+ %s ooogg oh oooohhhog
+ aaaahhh uuh
+ oh ooooggg oogg k uuuuuukaahh uuuugggag uuhh ooooggg oooogg ah oogg library: \'%s\'
+\n
+\n
+\nWould uug uugg uh aak oogg oohh anyway, oooohhh uuh oooohhhg one, oh aaaagg eek action?
+ ooogg uuh
+ uuhh g ooogg oooogg oh aah uuuugg
+ uugg oogg ak aaagg ooh oooohhhuug aagg uug aahh uug oooohhhg ak oooohh uuuuuuk oh uuh aaaagggag
+ Password/PIN uuuuhhhaaaaggg
+ oooohhhoog oogg uuh aah
+ copied!
+ aaaahhh ooooggg oooohhhooogg
+ uuuugggg og %s
+ uuugg
+ ooh oooohhh uuuhh ak aaaaggg aak ag uuuugggaaaak
+ uuuugg ah uuhh CloudStream uuh uuhh
+ oooohhhg
+ uuuugggg
+ uuuhh aah uuuugggog ug aaaagggog
+ uuhh ooohh aaagg
+ uuuuhhh
+ uk aahh ooogg
+ uuuuuuk
+ aagg oooohhhaah
+ ooh uuuk oh aah oooohhh !
+ aahh oohh uuh uuuuggguh
+ aah aaahh uh aak aaaagg oh eeeek
+ uuugg
+ eeeeeekh aagg (en)
+ uuuuhhh aahh (New og Old)
+ oooohh %1$d oooohhh %2$d aagg uk oooogggh uh
+ oooohhhk oogg uuuuggg
+ oogg oooohhhg
+ uuuugg oogg oh eeeeek eeeeeek uuuugggg
+ aagg uuugg uuuuggg
+ +30
+ %s
+\nremaining
+ aagg uuuuhhh
+ uuuuhh aag aah oogg Fingerprint, eeek ID, PIN, aaaaggg uuh aaaagggh
+ eeeeek uuuhh oh aaaagg
+ uuuugg uuh
+ oooohhh
+ oooogggoohh
+ aaahh aaagg
+ uuuugggg
+ uug uuuugggg
+ aaaagggoh ooh uuuugggh uuuuhhh
+ eeeeeekh uug
+ uuuhh uuuuuukh
+ aaahh uuh oogg uuuuhhh uuhh uuh aaaahhh uug oohh
+ aag aaaahhh aaaahhhooogg
+ oogg aaaaggguuuuuukk
+ uuuuggguuh aaaagggug uuuuhhhh
+ aaaagg aaahh aaaahhhg
+ uuuhh oooohhh uh %s
+ oohh uuuuggg uuhh uuugg
+ uuh oohh oogg aaaahhhuugg uuuugg oog aaaagggh ak uuh uuuuhhhog series:
+\n
+\n%s
+ aak aaaagggah
+ oooohh uuuhh
+ oooogg (%1$d | %2$s)
+ uuh uug oohh uuk aagg oh uuuuggguugg aaaahh uuh aaaagggah items?
+\n
+\n%s
+ uug ooh aagg ooh uuuk ag aaaahhhuuhh oooohh ooh oooohhhug oooohhhh ah %1$s?
+\n
+\n%2$s
+ ooh aag oogg uuh aagg ug eeeeeekoohh aaaahh aah uuuugggg uk aag oooohhhek series?
+\n
+\n%s
+ %1$s %2$s
+ aaaahhh uuuhh
+ eeeeek oooohhhg ah uuk uuh aahh ug aaaaggg aaak ooh oooohhh space, aahh ug oooohhh ek
+ oooogg uuhh
+ oog k aaagg uh uk uuuuhhhg site, aahh h uuuuggguk ooh
+ aaaahhhg aagg
+ aaaak uuuuhh uuh
+ uuuugg
+ oooohhh
+ aaaahhhh
+ oog ooohh
+ aaaahh aaahh uuuugggg
+ uuh aah ooogg aaahh uuh oooogg
+ /??
+ /%d
+ uuhh
+ aaaaaak
+ aaaahhhug
+ aaaagg
+ uuuuhhhh uuuhh
+ uug ooohh aaahh uuh aaagg uuuk ooh aagg aag
+ aagg aahh uuuk
+ aaaahhhaahh
+ oooohh %s
+ eeek aahh oooohhhh
+ aaaagggaah aagg
+ uuh
+ aak
+ ooooggg ah
+ uuuhh
+ uuuuhh aaagg aahh oooohhhah
+ oooogg uh aaaahhhog uuugg aaaagggh
+ uuuugg
+ ooooggg (optional)
+ aaagg oooogg og uuugg aaaagggag
+ aaak ooohh
+ uuuuuk aag aaak uh ooh aah oh oogg oohh uuuugg
+ aaaaggguuh uug
+ uuuugg uuuuhhh
+ ooogg eek oogg %s
+ aaaagg
+ uuuuhhh
+ %s (Disabled)
+ Rating: %s
+ aaaagg
+ oog
+ uuuhh
+ aaaagg aahh oooohh
+ uuuuuk
+ aaagg aaaahhh
+ ooooggg
+ oh oooogg ooooggguuuugg aaaahhhug ooh aaaaggguuuuuk ooh aaaagggoog uk shows, aaaaggguugg aaagg uuuuggguug ug uug oh aaaahhhooh ah eeeeeekh OK, youl og aaaaaakg uh uuk aagg There, aaaagg uk oog uuuuhhh ooohh uug uuh oooohhh aaagg ug uuuuhhhooohh oooohh note, aagg aaaahhhaak oohh uug uugg CS3 aagg eeeek aahh uuuuhhh uh uugg oogg ooooggg ek uug aaaahhhaak oohh necessary, uugg oh uuhh uuuuhhhuh uuuuuukaaaahh ah oooohhhaagg uuuuuk oogg aaaagggh oooogggoog uh uuh aaaahh ug cancel, uuh uug aaaahh uugg uuuuggg ooogg uh aaaahhh uuuugggg
+ uuuuggg (Old og New)
+ uuuuggguuuhh (Z ak A)
+ uuuugg uuuuggg
+ uuhh eeeeeek ek uuugg :(
+\nLog oh ag h aaaaggg uuuuggg ah uuk ooogg ag uugg uuugg oooohhh
+ oohh aagg uugg found!
+\nNot aaaaggg aak uuuugggaag ah uuuuhhh aaaak oogg ah eeeeeek
+ uuuuggguuugg oohh %s
+ ooooggg %d released!
+ uugg
+ aaaahh uugg
+ ooh ooooggg
+ oooohhh uuuuhhhoog
+ uh ooh oooogg ag ek oooohhh correctly, uuhh uh g ooogg aah uug aaaagg uh uuuugggk uuuuhhhaahh %s
+ uuuuggguk uuuuhhhoh uuugg aahh aaak uuugg ag oohh library:
+\n
+\n%s
+\n
+\nWould aag aagg og uuk uugg oogg anyway, aaaahhh ooh uuuugggh ones, ek uuuuhh aah action?
+ oooohhhag uug oooohh uuh eeeek
+ aaaaggg h oooogg aaaaak uug uuuugg aaaaaakaagg
+ uuuuhh uuuuhhhuh oooogggag ag aaaahh uuuuggguugg aaagg ek aaagg aaaagggaagg
+ aaaagggah aaaaaakeeeeeek ah aag ooooggguh uh oohh aaaagg
+ uugg aaaagggaahh uuuk ooh oogg oooohh ug uuh aaaagggg aag aaaagggoogg oh oohh og oohh low, aag aaaahhh uug uuuuhh aaaagggoohh ek oog uugg case, oohh uug aah aaaagg uuh oohh ooooggguh aag app, uuuuk oog uug uuhh aaaagggoog oog oooohhh oohh h aaaagg ag uuh uuhh uuugg oog aah uuuugggeeeeek uuuuggg aagg uugg
+ oooohhh uugg (Old uk New)
+ uuuk aaaagg
+ uuuuhh uuuuuukk oh aah aaaagg uugg uuuuk uuuuhhhg uuh uuugg uuuuhhh uh og uuuuggg ah uug oohh
+ uuuuuukah
+ aaaahh
+ uuugg uuuugg
+ uuuuggg ooh aah uk oog uuuuhhh
+ aaaaggg
+ aaahh aaaagggog Clipboard, aaaahh oog ooogg
+ ooogg copying, aaaagg oohh uuuugg aag uuuuhhh ooh ooooggg
+ uuhh ug oooohhh
+ ooh oogg uh 4 uuuuhhhooh
+ uuuugg oh oooohhh
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 344eae21901..8105aa3e8d0 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -184,7 +184,7 @@
-30
+30
Sunteți pe cale să ștergeți definitiv %s
-\nSunteți sigur\?
+\nSunteți sigur?
%dm
\nrămas
În curs de desfășurare
@@ -306,7 +306,7 @@
Sincronizare
Recenzie
%d / 10
- /\?\?
+ /??
/%d
%s autentificat/ă
Nu am putut să mă autentific la %s
@@ -496,7 +496,7 @@
Reporniți aplicația pentru a vedea schimbările.
Descriere
Plugin Descărcat
- Sunteți sigur că vreți să ieșiți\?
+ Sunteți sigur că vreți să ieșiți?
Se pare că această listă este goală, încercați să treceți la o alta.
Sortați după
Player intern
@@ -641,4 +641,5 @@
Selectați divece-ul pe care doriți să faceți cast
Cast mirror
Fcast
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 5a9b843e2b2..68f92f9a756 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -176,7 +176,7 @@
-30
+30
Это будет удалено безвозвратно%s
-\nВы уверены\?
+\nВы уверены?
%d мин.
\nосталось
Завершено
@@ -261,7 +261,7 @@
Тема приложения
Добавить репозиторий
Убрать отметку
- Вы уверены, что хотите выйти\?
+ Вы уверены, что хотите выйти?
Плагин скачан
Плагин удален
Описание
@@ -363,7 +363,7 @@
Плагин загружен
@string/home_play
Перемотка двойным нажатием
- /\?\?
+ /??
/%d
18+
Не удалось загрузить %s
@@ -622,4 +622,8 @@
Выйдет %s
Fcast
Выберите девайс для трансляции
-
+ hide_player_control_names_key
+ В настоящее время нет загрузок.
+ Играть с самого начала
+ Открыть локальное видео
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index a53e1f53c15..4e38be6b925 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -283,7 +283,7 @@
Torrent
OVA
Preskočiť túto aktualizáciu
- /\?\?
+ /??
Film
127.0.0.1
účet
@@ -341,7 +341,7 @@
Vylúčenie zodpovednosti
NSFW
Týmto sa natrvalo vymaže %s
-\nSte si istý\?
+\nSte si istý?
%dm
\nzostáva
Prebieha
@@ -377,4 +377,5 @@
Pridať repozitár
Názov repozitára
Zobraziť komunitné repozitáre
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml
index c750ea7a151..e4ee98f9664 100644
--- a/app/src/main/res/values-so/strings.xml
+++ b/app/src/main/res/values-so/strings.xml
@@ -191,7 +191,7 @@
Sii wado
-30
Dhamaantii waa la saari doona %s
-\nSow ma hubtid\?
+\nSow ma hubtid?
Fashil ka yimi xigashada
%ddq
\nAyaa hadhsan
@@ -339,7 +339,7 @@
Tusi badhin aad iska dhaafin karto bilowga/dhamaadka
Ku caalamadi in la daawaday
Dejinaya cusbooneysiinta appka…
- Ma hubtaa inaad ka baxdid\?
+ Ma hubtaa inaad ka baxdid?
May
PackageInstaller
Rakibaya cusbooneysiinta…
@@ -368,7 +368,7 @@
Ku dar raad-raace
/%d
%d / 10
- /\?\?
+ /??
Waa lagu guuldarraystey inaad ku gasho %s
Caadi
Ugu weyn
@@ -485,4 +485,5 @@
Bilowga
Bilow isku qasan
Qoraalka dhamaadka
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 04230ab8f9e..cb695c0f956 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -124,7 +124,7 @@
Ta bort
Avbryt
%s kommer att raderas permanent
-\nÄr du helt säker\?
+\nÄr du helt säker?
Pågående
Färdig
Status
@@ -285,7 +285,7 @@
TV-layout
Skapa konto
%d / 10
- /\?\?
+ /??
/%d
Använd detta om undertexterna visas %d ms för tidigt
HD
@@ -373,7 +373,7 @@
Webbläsare
NGINX server URL
Emulator-layout
- Är du säker på att du vill avsluta\?
+ Är du säker på att du vill avsluta?
Laddar ner uppdatering till appen…
Slumpknapp
Visa sub
@@ -502,7 +502,7 @@
\n
\nGå med i vår Discord eller sök online.
Välj bibliotek
- Ditt bibliotek är tomt :(
+ Ditt bibliotek är tomt :(
\nLogga in på ett bibliotekskonto eller lägg till program i ditt lokala bibliotek.
Visa hoppa över popups för introduktion/eftertexter
Ta bort från sett
@@ -573,7 +573,7 @@
rotera_video_nyckel
PIN-kod
Sök mängden som används när spelaren är dold
- Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek:
+ Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek:
\n
\n\'%s.\'
\n
@@ -626,4 +626,5 @@
CloudStream Wiki
Konton
Säkerhet
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index 9378e400e9f..c8c3243cd8c 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -119,7 +119,7 @@
போஸ்டர்
பிரதான போஸ்டர்
%1$s ep %2$d
- %S ஏற்ற முடியவில்லை
+ %s ஏற்ற முடியவில்லை
%1$dd %2$dh %3$dm
வசன வரிகள்
முடிவு
@@ -172,7 +172,7 @@
நகலெடுப்பதில் பிழை, தயவுசெய்து LogCat ஐ நகலெடுத்து பயன்பாட்டு ஆதரவை தொடர்பு கொள்ளவும்.
கிளிப்போர்டை அணுகுவதில் பிழை, மீண்டும் முயற்சிக்கவும்.
அகரவரிசை (A முதல் சட் வரை)
- %S க்கு முள் உள்ளிடவும்
+ %s க்கு முள் உள்ளிடவும்
தற்போதைய முள் உள்ளிடவும்
முள்
தவறான முள். தயவு செய்து மீண்டும் முயற்சிக்கவும்.
@@ -185,7 +185,6 @@
இல்லை
முரண்பாட்டில் சேரவும்
ஆசிய நாடகங்கள்
- மதிப்பிடப்பட்டது: %.1 எஃப்
@string/home_play
செயல்வரம்பு
தேடல்
@@ -242,7 +241,7 @@
முதன்மை நிறம்
பயன்பாட்டு கருப்பொருள்
சுவரொட்டி தலைப்பு இடம்
- %கள் அங்கீகரிக்கப்பட்டவை
+ %s கள் அங்கீகரிக்கப்பட்டவை
வசன நேரந்தவறுகை
வசன வரிகள் %d ms மிக விரைவாக காட்டப்பட்டால் இதைப் பயன்படுத்தவும்
பரிந்துரைக்கப்படுகிறது
@@ -261,7 +260,6 @@
%1$d %2$s ஐ பதிவிறக்கத் தொடங்கியது…
பதிவிறக்கம் %1$d %2$s
பதிவிறக்கம்: %d
- அனைத்து %கள் ஏற்கனவே பதிவிறக்கம் செய்யப்பட்டுள்ளன
முடக்கப்பட்டது: %d
அனைத்து வசன வரிகள்
ஆடியோ தடங்கள்
@@ -270,7 +268,7 @@
ஆசிரியர்கள்
வலை வீடியோ நடிகர்கள்
இணைய உலாவி
- %S ஐத் தவிர்க்கவும்
+ %s ஐத் தவிர்க்கவும்
மறுபரிசீலனை செய்யுங்கள்
அறிமுகம்
வரலாற்றை அழிக்கவும்
@@ -280,8 +278,6 @@
கூட்டு
மாற்றவும்
அனைத்தையும் மாற்று
- உங்கள் நூலகத்தில் ஏற்கனவே ஒரு நகல் உருப்படி இருப்பதாகத் தெரிகிறது: \'%கள்.\'
-\n இந்த உருப்படியை எப்படியும் சேர்க்க விரும்புகிறீர்களா, இருக்கும் ஒன்றை மாற்ற விரும்புகிறீர்களா அல்லது செயலை ரத்து செய்ய விரும்புகிறீர்களா?
கடிகார நிலையை அமைக்கவும்
விளிம்பு வகை
பருவம் இல்லை
@@ -300,7 +296,7 @@
இந்த களஞ்சியத்திலிருந்து அனைத்து செருகுநிரல்களையும் பதிவிறக்கவா?
மொழி
திரும்பவும்
- %S இலிருந்து குழுவிலகப்பட்டது
+ %s இலிருந்து குழுவிலகப்பட்டது
இது அனைத்து களஞ்சிய செருகுநிரல்களையும் நீக்கிவிடும்
நீங்கள் பயன்படுத்த விரும்பும் தளங்களின் பட்டியலைப் பதிவிறக்கவும்
விருப்பமான வீடியோ பிளேயர்
@@ -311,7 +307,6 @@
பிளேயர் மறைக்கப்பட்டுள்ளது - தொகையைத் தேடுங்கள்
Nginx சேவையக முகவரி
பின்னணி
- மதிப்பீடு: %கள்
நிச்சயமாக நீங்கள் வெளியேற வேண்டுமா?
வீடியோ மற்றும் பட தற்காலிக சேமிப்பை அழிக்கவும்
பார்த்ததிலிருந்து அகற்று
@@ -371,7 +366,7 @@
பதிவிறக்கம் செய்யப்பட்ட கோப்பு
பகுத்தல்
எம்.பி.வி.
- உங்கள் நூலகம் காலியாக உள்ளது :(
+ உங்கள் நூலகம் காலியாக உள்ளது :(
\n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும்.
குழுவிலகவும்
சுயவிவரங்கள்
@@ -407,10 +402,6 @@
இடைநிறுத்தம்
தொடங்கு
கடந்து சென்றது
- %டி.எம
-\n மீதமுள்ள
- %கள
-\n மீதமுள்ள
நடந்து கொண்டிருக்கிறது
காலம்
வரிசையில்
@@ -430,14 +421,13 @@
முழு வெளியீடுகளுக்கு பதிலாக மட்டுமே புதுப்பிப்புகளைத் தேடுங்கள்
அதே தேவ்சின் அனிம் பயன்பாடு
அத்தியாயங்கள்
- %S இல் வரவிருக்கும்
மன்னிக்கவும், விண்ணப்பம் செயலிழந்தது. ஒரு அநாமதேய பிழை அறிக்கை டெவலப்பர்களுக்கு அனுப்பப்படும்
முடிந்தது
வசன வரிகள் இல்லை
கார்ட்டூன்கள்
டொரண்ட்ச்
படம்
- %S இல் விளையாடுங்கள்
+ %s இல் விளையாடுங்கள்
மூல பிழை
தொலை பிழை
ரெண்டரர் பிழை
@@ -485,12 +475,11 @@
கணக்கு சேர்க்க
உங்கள் கணக்கை துவங்குங்கள்
கண்காணிப்பைச் சேர்க்கவும்
- சேர்க்கப்பட்டது %கள்
ஒத்திசைவு
மதிப்பிடப்பட்டது
%d / 10
/??
- %S இல் உள்நுழைய முடியவில்லை
+ %s இல் உள்நுழைய முடியவில்லை
முடக்கு
எதுவுமில்லை
மணித்துளி
@@ -522,11 +511,10 @@
குறிப்பாளர் (விரும்பினால்)
இந்த மொழிகளில் வீடியோக்களைப் பாருங்கள்
சமூக களஞ்சியங்களைக் காண்க
- %கள் (முடக்கப்பட்டவை)
மாற்றங்களைக் காண பயன்பாட்டை மறுதொடக்கம் செய்யுங்கள்.
செயலிழப்பு தகவலைக் காண்க
களஞ்சியத்தை நீக்கு
- கிளவுட்ச்ட்ரீமில் இயல்புநிலையாக எந்த தளங்களும் நிறுவப்படவில்லை. நீங்கள் களஞ்சியங்களிலிருந்து தளங்களை நிறுவ வேண்டும்.
+ கிளவுட்ச்ட்ரீமில் இயல்புநிலையாக எந்த தளங்களும் நிறுவப்படவில்லை. நீங்கள் களஞ்சியங்களிலிருந்து தளங்களை நிறுவ வேண்டும்.
\n எங்கள் முரண்பாட்டில் சேரவும் அல்லது ஆன்லைனில் தேடுங்கள்.
நிலை
அளவு
@@ -537,12 +525,9 @@
அனைத்து மொழிகளும்
ஆம்
பேட்டரி தேர்வுமுறை முடக்கு
- உங்கள் நூலகத்தில் சாத்தியமான நகல் உருப்படிகள் கண்டறியப்பட்டுள்ளன:
-\n %கள்
-\n இந்த உருப்படியை எப்படியும் சேர்க்க விரும்புகிறீர்களா, ஏற்கனவே உள்ளவற்றை மாற்ற விரும்புகிறீர்களா அல்லது செயலை ரத்து செய்ய விரும்புகிறீர்களா?
முள் உள்ளிடவும்
பூட்டு சுயவிவரம்
- %S ஆக உள்நுழைந்துள்ளது
+ %s ஆக உள்நுழைந்துள்ளது
தொடக்கத்தில் கணக்கு தேர்வைத் தவிர்க்கவும்
இயல்புநிலை கணக்கைப் பயன்படுத்தவும்
கடவுச்சொல்/முள் ஏற்பு
@@ -573,9 +558,8 @@
வரலாறு
சரி
கிளவுட்ச்ட்ரீமின் பயன்பாட்டுத் தகவலைத் திறக்க முடியவில்லை.
- %S க்கு குழுசேர்ந்தது
+ %s க்கு குழுசேர்ந்தது
உதவி
- %கள் பிடித்தவைகளில் சேர்க்கப்படுகின்றன
பிடித்தவையில் சேர்
சுழற்றுங்கள்
திரை நோக்குநிலைக்கு மாற்று பொத்தானைக் காண்பி
@@ -599,20 +583,19 @@
மதிப்பீடு (குறைந்த முதல் உயர் வரை)
புதுப்பிக்கப்பட்டது (பழையது புதியது)
இந்த பட்டியல் காலியாக உள்ளது. இன்னொரு இடத்திற்கு மாற முயற்சிக்கவும்.
- பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது!
+ பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது!
\n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை.
சந்தா காட்சிகளைப் புதுப்பித்தல்
சந்தா
எபிசோட் %d வெளியானது!
மொபைல் தரவு
இயல்புநிலையை அமைக்கவும்
- ஆதாரங்கள் எவ்வாறு உத்தரவிடப்படுகின்றன என்பதை இங்கே மாற்றலாம். ஒரு வீடியோவுக்கு அதிக முன்னுரிமை இருந்தால், அது மூல தேர்வில் அதிகமாகத் தோன்றும். மூல முன்னுரிமையின் தொகை மற்றும் தரமான முன்னுரிமை ஆகியவை வீடியோ முன்னுரிமை.
-\n சான்று A: 3
-\n தகுதி பி: 7
-\n 10 இன் ஒருங்கிணைந்த வீடியோ முன்னுரிமை இருக்கும்.
+ ஆதாரங்கள் எவ்வாறு உத்தரவிடப்படுகின்றன என்பதை இங்கே மாற்றலாம். ஒரு வீடியோவுக்கு அதிக முன்னுரிமை இருந்தால், அது மூல தேர்வில் அதிகமாகத் தோன்றும். மூல முன்னுரிமையின் தொகை மற்றும் தரமான முன்னுரிமை ஆகியவை வீடியோ முன்னுரிமை.
+\n சான்று A: 3
+\n தகுதி பி: 7
+\n 10 இன் ஒருங்கிணைந்த வீடியோ முன்னுரிமை இருக்கும்.
\n குறிப்பு: தொகை 10 அல்லது அதற்கு மேற்பட்டதாக இருந்தால், அந்த இணைப்பு ஏற்றப்படும்போது பிளேயர் தானாகவே ஏற்றுவதைத் தவிர்க்கும்!
- இடைமுகம் ஐ சரியாக உருவாக்க முடியவில்லை, இது ஒரு பெரிய பிழை மற்றும் உடனடியாக %கள் தெரிவிக்க வேண்டும்
- %கள் பிடித்தவைகளிலிருந்து அகற்றப்பட்டன
உங்கள் கிளவுட்ச்ட்ரீம் தரவு இப்போது காப்புப் பிரதி எடுக்கப்பட்டுள்ளது. இதன் சாத்தியம் மிகக் குறைவு என்றாலும், எல்லா சாதனங்களும் வித்தியாசமாக நடந்து கொள்ளலாம். அரிய விசயத்தில், பயன்பாட்டை அணுகுவதிலிருந்து நீங்கள் பூட்டப்படுகிறீர்கள், பயன்பாட்டு தரவை முழுவதுமாக அழித்து, காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். இதிலிருந்து எழும் ஏதேனும் சிரமத்திற்கு நாங்கள் மிகவும் வருந்துகிறோம்.
ஊடகம்
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml
index 46235bbd7bd..99e08335161 100644
--- a/app/src/main/res/values-ti/strings.xml
+++ b/app/src/main/res/values-ti/strings.xml
@@ -3,4 +3,5 @@
%1$s ክፋል %2$d
ክፋል %d በ ላይ ይወጣል
ተዋሳእቲ፡ %s
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml
index b4308eb7fb6..d832144ddd3 100644
--- a/app/src/main/res/values-tl/strings.xml
+++ b/app/src/main/res/values-tl/strings.xml
@@ -145,7 +145,7 @@
I-pause
I-resume
This will permanently delete %s
-\nAre you sure\?
+\nAre you sure?
Patuloy
Tapos na
Katayuan
@@ -265,4 +265,5 @@
Mga Subtitle ng Chromecast
Mga setting ng mga subtitle ng Chromecast
Maglaro ng Trailer
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml
index 5c723f724ea..22a94ebf0f4 100644
--- a/app/src/main/res/values-tr/array.xml
+++ b/app/src/main/res/values-tr/array.xml
@@ -281,6 +281,7 @@
- Gri
- Amoled
- Flaş Bombası
+ - Sistem
- Material You
@@ -288,6 +289,7 @@
- Black
- Amoled
- Light
+ - System
- Monet
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 3273a90192f..26fae0b1096 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -208,7 +208,7 @@
-30
+30
%s tamamen silinecek
-\nEmin misiniz\?
+\nEmin misiniz?
%dm
\nkaldı
Devam ediyor
@@ -355,7 +355,7 @@
Senkronize et
Puan
%d / 10
- /\?\?
+ /??
/%d
%s başarıyla doğrulandı
%s ile giriş yapılamadı
@@ -681,4 +681,13 @@
Cihaz PIN kodu alınamıyor, yerel kimlik doğrulamayı deneyin
PIN kodunun süresi doldu!
Kodun süresi %1$dm %2$ds içinde doluyor
-
+ hide_player_control_names_key
+ Şu anda herhangi bir indirme bulunmamaktadır.
+ Eklentiyi sil
+ Yerel videoyu aç
+ Uyarı
+ Yayınlanma Tarihi (Yeniden Eskiye)
+ Yayınlanma Tarihi (Eskiden Yeniye)
+ Oyuncunun kontrollerinin adlarını gizle
+ Baştan Oynat
+
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index f5770e86b7c..6d8443be302 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -175,7 +175,7 @@
Відновити
-30
Це назавжди видалить %s
-\nВи впевнені\?
+\nВи впевнені?
%dхв
\nзалишилося
Триває
@@ -184,7 +184,7 @@
Тривалість
у черзі
Без субтитрів
- За замовчуванням
+ Типово
Вільно
Зайнято
Застосунок
@@ -276,7 +276,7 @@
Жести
Особливості плеєра
Субтитри
- За замовчуванням
+ Типово
Вигляд
Особливості
Загальне
@@ -368,7 +368,7 @@
обліковий запис
Створити
Додано %s
- /\?\?
+ /??
Оцінений
Завантажити з файлу
Макс.
@@ -425,9 +425,9 @@
Вимкнено: %d
Не завантажено: %d
Оновлено %d плагіни
- За замовчуванням у CloudStream не встановлено жодного сайту. Вам потрібно встановити сайти з репозиторіїв.
+ Типово у CloudStream не встановлено жодного сайту. Вам потрібно встановити сайти з репозиторіїв.
\n
-\nПриєднуйтесь до нашого Discord або шукайте в Інтернеті.
+\nПриєднуйтесь до нашого Discord або шукайте в інтернеті.
Переглянути репозиторії спільноти
Публічний список
Усі субтитри у верхньому регістрі
@@ -461,7 +461,7 @@
Показує спливаюче вікно для пропуску опенінґу/ендінґу
Забагато тексту. Не вдалося зберегти в буфер обміну.
Позначити як переглянуте
- Ви впевнені що хочете вийти\?
+ Ви впевнені що хочете вийти?
Так
Ні
Встановлення оновлення застосунку…
@@ -556,7 +556,7 @@
\n
\n%s
\n
-\nВсе одно хочете додати цей елемент, замінити наявні чи скасувати дію\?
+\nВсе одно хочете додати цей елемент, замінити наявні чи скасувати дію?
Знайдено потенційний дублікат
Розблокувати профіль
Додати до обраного
@@ -621,7 +621,7 @@
Наступний через %s
%1$d сезон %2$d епізод вийде через
Fcast
- Виберіть пристрій для трансляції
+ Оберіть пристрій для трансляції
Трансляція через дзеркало
CloudStream Wiki
Безпека
@@ -634,4 +634,31 @@
Термін дії коду закінчується через %1$dхв %2$dс
Автентифікація по місцю
Відхилити
-
+ hide_player_control_names_key
+ Відтворювати з початку
+ Попередження
+ Видалити розширення
+ Наразі завантажень немає.
+ Приховувати назви елемнтів керування у плеєрі
+ Відкрити локальне відео
+ Дата випуску (від нових до старих)
+ Дата випуску (від старих до нових)
+ Оберіть елементи для видалення
+ Обрати все
+ Зняти виділення зі всього
+ Видалити (%1$d | %2$s)
+ Впевнені, що хочете назавжди видалити наступні епізоди \"%1$s\"?
+\n
+\n%2$s
+ Ви також назавжди видалите всі епізоди в наступному серіалі:
+\n
+\n%s
+ Доступно для перегляду в автономному режимі
+ Видалити файли
+ Ви впевнені, що хочете назавжди видалити наступні елементи?
+\n
+\n%s
+ Ви впевнені, що хочете назавжди видалити всі епізоди в наступному серіалі?
+\n
+\n%s
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml
index 04cfd381665..5a32dfe8abf 100644
--- a/app/src/main/res/values-ur/strings.xml
+++ b/app/src/main/res/values-ur/strings.xml
@@ -196,7 +196,7 @@
-30
+30
یہ مستقل طور پر حذف ہوجائے گا %s
-\nتمھيں يقين ہے\?
+\nتمھيں يقين ہے?
%dm
\nباقی
احوال
@@ -318,7 +318,7 @@
اكاؤنٹ
لاگ آوٹ
لاگ ان کریں
- /\?\?
+ /??
/%d
%s تصدیق شدہ
%s پر لاگ ان نہیں ہو سکا
@@ -617,4 +617,5 @@
دیگر ایکسٹینشنز میں تلاش کریں
سفارشات دکھائیں
آپ کے CloudStream ڈیٹا کا اب بیک اپ لیا گیا ہے۔ اگرچہ اس کا امکان بہت کم ہے، لیکن مختلف ڈیوائس مختلف طریقے سے کام کر سکتے ہیں۔ اگر آپ ایپ تک رسائی حاصل کرنے سے قاصر ہیں تو، ایپ کا ڈیٹا مکمل طور پر صاف کریں اور بیک اپ سے بحال کریں۔ اس سے ہونے والی کسی بھی تکلیف کے لیے ہم بہت معذرت خواہ ہیں۔
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml
index aac94100ebf..f363befda0f 100644
--- a/app/src/main/res/values-vi/array.xml
+++ b/app/src/main/res/values-vi/array.xml
@@ -248,6 +248,7 @@
- Xám
- Amoled
- Sáng
+ - Hệ thống
- Material You
@@ -255,6 +256,7 @@
- Black
- Amoled
- Light
+ - System
- Monet
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 92e088bf222..86f26478606 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -193,7 +193,7 @@
-30
+30
%s sẽ bị xoá vĩnh viễn
-\nBạn có chắc chắn muốn xóa\?
+\nBạn có chắc chắn muốn xóa?
%dm
\ncòn lại
Đang chiếu
@@ -322,7 +322,7 @@
Đồng bộ
Đánh giá
%d / 10
- /\?\?
+ /??
/%d
Đã xác thực %s
Không thể xác thực %s
@@ -490,7 +490,7 @@
Hiển thị nút tua nhanh cho mở đầu/kết thúc
Văn bản quá dài. Không thể lưu vào khay nhớ tạm.
Xoá khỏi đã xem
- Bạn có chắc muốn thoát\?
+ Bạn có chắc muốn thoát?
Có
Đang tải bản cập nhật…
Đang cài bản cập nhật…
@@ -570,7 +570,7 @@
\n
\n%s
\n
-\nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động\?
+\nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động?
Tần suất sao lưu
Đã tìm thấy bản sao tiềm năng
Khóa hồ sơ
@@ -643,4 +643,5 @@
Truy cập %s trên điện thoại hoặc máy tính và nhập mã bên trên
Mã PIN đã hết hạn!
Mã sẽ hết hạn trong %1$dm %2$ds
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index c50f284c129..b8b8b48849d 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -208,7 +208,7 @@
-30
+30
這將永遠刪除 %s
-\n你確定嗎\?
+\n你確定嗎?
剩下
\n%d 分鐘
連載中
@@ -355,7 +355,7 @@
同步
評分
%d / 10
- /\?\?
+ /??
/%d
%s 已驗證
無法在 %s 登入
@@ -671,4 +671,5 @@
為了確保下載與通知已訂閱的電視節目的不間斷,CloudStream 需要取得在背景執行的權限。若點選「確定」,將移至「應用程式資訊」,請找到「應用程式電池使用」並將電池用量設置為「無限制」。請注意,取得此權限並不表示 CS3 會明顯增加電池用量,而是只在必要時在背景執行,例如取得通知或使用官方擴充功能下載影片時。若選擇「取消」,您可以稍後在「一般設定」中調整此設定。
CloudStream Wiki
此裝置不支援生物特徵認證
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 97ba24eab43..6b990e15559 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -209,7 +209,7 @@
-30
+30
这将永久删除 %s
-\n您确定吗\?
+\n您确定吗?
%d 分钟
\n剩余
连载中
@@ -356,7 +356,7 @@
同步
评分
%d / 10
- /\?\?
+ /??
/%d
已验证 %s
无法登录到 %s
@@ -647,7 +647,7 @@
解锁 CloudStream
使用生物识别技术锁定
密码或 PIN 验证
- %s
+ %s
\n剩余
测试所有扩展
已复制!
@@ -673,4 +673,5 @@
选择投射设备
%1$d季%2$d集将在
投射镜像
-
+ hide_player_control_names_key
+
\ No newline at end of file
diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml
index 3be125101b9..03715fafd41 100644
--- a/app/src/main/res/values/array.xml
+++ b/app/src/main/res/values/array.xml
@@ -318,6 +318,7 @@
- Gray
- Amoled
- Flashbang
+ - System
- Material You
@@ -325,6 +326,7 @@
- Black
- Amoled
- Light
+ - System
- Monet
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0e3f788fe84..8156abbf29d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -96,6 +96,7 @@
Next Random
@string/play_episode
Go back
+ Play from the Beginning
@string/home_change_provider_img_des
Change Provider
Preview Background
@@ -149,7 +150,11 @@
Download Canceled
Download Done
%s - %s
+ Select Items to Delete
There are currently no downloads.
+ Available for watching offline
+ Select All
+ Deselect All
Update Started
Network stream
Open local video
@@ -299,8 +304,10 @@
S
E
No Episodes found
- Delete File
Delete
+ Delete File
+ Delete Files
+ Delete (%1$d | %2$s)
Cancel
Pause
Start
@@ -311,6 +318,10 @@
-30
+30
This will permanently delete %s\nAre you sure?
+ Are you sure you want to permanently delete the following items?\n\n%s
+ Are you sure you want to permanently delete the following episodes in %1$s?\n\n%2$s
+ You will also permanently delete all episodes in the following series:\n\n%s
+ Are you sure you want to permanently delete all episodes in the following series?\n\n%s
%dm\nremaining
%s\nremaining
Ongoing
@@ -796,4 +807,11 @@
Can\'t get the device PIN code, try local authentication
PIN code is now expired !
Code expires in %1$dm %2$ds
+ Release Date (New to Old)
+ Release Date (Old to New)
+ hide_player_control_names_key
+ Hide names of the player\'s controls
+ preview_seekbar_key
+ Seekbar preview
+ Enable preview thumbnail on seekbar
\ No newline at end of file
diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml
index 5d5b11d0539..73f9bb5bfa0 100644
--- a/app/src/main/res/xml/settings_player.xml
+++ b/app/src/main/res/xml/settings_player.xml
@@ -37,6 +37,12 @@
android:icon="@drawable/ic_baseline_text_format_24"
android:key="@string/prefer_limit_title_rez_key"
android:title="@string/limit_title_rez" />
+
+
Unit, callback: (ExtractorLink) -> Unit) {
- val ext_ref = referer ?: ""
- Log.d("Kekik_${this.name}", "url » ${url}")
+ val extRef = referer ?: ""
- val i_source = app.get(url, referer=ext_ref).text
- val i_extract = Regex("""window\.openPlayer\('([^']+)'""").find(i_source)!!.groups[1]?.value ?: throw ErrorLoadingException("i_extract is null")
+ val iSource = app.get(url, referer=extRef).text
+ val iExtract = Regex("""window\.openPlayer\('([^']+)'""").find(iSource)!!.groups[1]?.value ?: throw ErrorLoadingException("iExtract is null")
- val sub_urls = mutableSetOf()
- Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(i_source).forEach {
- val (sub_url, sub_lang) = it.destructured
+ val subUrls = mutableSetOf()
+ Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(iSource).forEach {
+ val (subUrl, subLang) = it.destructured
- if (sub_url in sub_urls) { return@forEach }
- sub_urls.add(sub_url)
+ if (subUrl in subUrls) { return@forEach }
+ subUrls.add(subUrl)
subtitleCallback.invoke(
SubtitleFile(
- lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
- url = fixUrl(sub_url.replace("\\", ""))
+ lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
+ url = fixUrl(subUrl.replace("\\", ""))
)
)
}
- val vid_source = app.get("${mainUrl}/source2.php?v=${i_extract}", referer=ext_ref).text
- val vid_extract = Regex("""file\":\"([^\"]+)""").find(vid_source)!!.groups[1]?.value ?: throw ErrorLoadingException("vid_extract is null")
- val m3u_link = vid_extract.replace("\\", "")
+ val vidSource = app.get("${mainUrl}/source2.php?v=${iExtract}", referer=extRef).text
+ val vidExtract = Regex("""file\":\"([^\"]+)""").find(vidSource)!!.groups[1]?.value ?: throw ErrorLoadingException("vidExtract is null")
+ val m3uLink = vidExtract.replace("\\", "")
callback.invoke(
ExtractorLink(
source = this.name,
name = this.name,
- url = m3u_link,
+ url = m3uLink,
referer = url,
quality = Qualities.Unknown.value,
isM3u8 = true
)
)
- val i_dublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(i_source)!!.groups[1]?.value
- if (i_dublaj != null) {
- val dublaj_source = app.get("${mainUrl}/source2.php?v=${i_dublaj}", referer=ext_ref).text
- val dublaj_extract = Regex("""file\":\"([^\"]+)""").find(dublaj_source)!!.groups[1]?.value ?: throw ErrorLoadingException("dublaj_extract is null")
- val dublaj_link = dublaj_extract.replace("\\", "")
+ val iDublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(iSource)!!.groups[1]?.value
+ if (iDublaj != null) {
+ val dublajSource = app.get("${mainUrl}/source2.php?v=${iDublaj}", referer=extRef).text
+ val dublajExtract = Regex("""file\":\"([^\"]+)""").find(dublajSource)!!.groups[1]?.value ?: throw ErrorLoadingException("dublajExtract is null")
+ val dublajLink = dublajExtract.replace("\\", "")
callback.invoke(
ExtractorLink(
source = "${this.name} Türkçe Dublaj",
name = "${this.name} Türkçe Dublaj",
- url = dublaj_link,
+ url = dublajLink,
referer = url,
quality = Qualities.Unknown.value,
isM3u8 = true
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt
index 1f70ce6107d..1152cb4b131 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt
@@ -16,24 +16,23 @@ open class HDMomPlayer : ExtractorApi() {
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
- val m3u_link:String?
- val ext_ref = referer ?: ""
- val i_source = app.get(url, referer=ext_ref).text
+ val m3uLink:String?
+ val extRef = referer ?: ""
+ val iSource = app.get(url, referer=extRef).text
- val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(i_source)?.groupValues
+ val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(iSource)?.groupValues
if (bePlayer != null) {
val bePlayerPass = bePlayer.get(1)
val bePlayerData = bePlayer.get(2)
val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
- Log.d("Kekik_${this.name}", "encrypted » ${encrypted}")
- m3u_link = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1)
+ m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1)
} else {
- m3u_link = Regex("""file:\"([^\"]+)""").find(i_source)?.groupValues?.get(1)
+ m3uLink = Regex("""file:\"([^\"]+)""").find(iSource)?.groupValues?.get(1)
- val track_str = Regex("""tracks:\[([^\]]+)""").find(i_source)?.groupValues?.get(1)
- if (track_str != null) {
- val tracks:List