diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f52d6e5e..b0798e44 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,7 +50,8 @@ android { } } - compileSdk = 33 + // https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading + compileSdk = 33 // android 14 is fucked buildToolsVersion = "34.0.0" defaultConfig { @@ -58,8 +59,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 59 - versionName = "4.1.8" + versionCode = 62 + versionName = "4.2.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -154,25 +155,29 @@ repositories { dependencies { implementation("com.google.android.mediahome:video:1.0.0") implementation("androidx.test.ext:junit-ktx:1.1.5") - testImplementation("org.json:json:20180813") + testImplementation("org.json:json:20230618") - implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.core:core-ktx:1.10.1") // need 34 for higher implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0 // dont change this to 1.6.0 it looks ugly af implementation("com.google.android.material:material:1.5.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + + // need 34 for higher implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") implementation("androidx.navigation:navigation-ui-ktx:2.6.0") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") + testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:core") - //implementation("io.karn:khttp-android:0.1.2") //okhttp instead + // implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") + // DONT UPDATE, WILL CRASH ANDROID TV ???? implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") implementation("androidx.preference:preference-ktx:1.2.0") @@ -199,28 +204,26 @@ dependencies { // Custom ffmpeg extension for audio codecs implementation("com.github.recloudstream:media-ffmpeg:1.1.0") - //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") - // Bug reports - implementation("ch.acra:acra-core:5.11.0") - implementation("ch.acra:acra-toast:5.11.0") + implementation("ch.acra:acra-core:5.11.2") + implementation("ch.acra:acra-toast:5.11.2") - compileOnly("com.google.auto.service:auto-service-annotations:1.0") + compileOnly("com.google.auto.service:auto-service-annotations:1.1.1") //either for java sources: - annotationProcessor("com.google.auto.service:auto-service:1.0") + annotationProcessor("com.google.auto.service:auto-service:1.1.1") //or for kotlin sources (requires kapt gradle plugin): - kapt("com.google.auto.service:auto-service:1.0") + kapt("com.google.auto.service:auto-service:1.1.1") // subtitle color picker implementation("com.jaredrummler:colorpicker:1.1.0") - //run JS + // run JS // do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not // available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown implementation("org.mozilla:rhino:1.7.13") // TorrentStream - //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") + // implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") // Downloading implementation("androidx.work:work-runtime:2.8.1") @@ -231,16 +234,16 @@ dependencies { // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 - implementation("org.conscrypt:conscrypt-android:2.2.1") + implementation("org.conscrypt:conscrypt-android:2.5.2") // Util to skip the URI file fuckery 🙏 implementation("com.github.LagradOst:SafeFile:0.0.5") // API because cba maintaining it myself - implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") + implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // debugImplementation because LeakCanary should only run in debug builds. - //debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") + // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") // for shimmer when loading implementation("com.facebook.shimmer:shimmer:0.5.0") @@ -252,7 +255,7 @@ dependencies { // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev // this should be updated frequently to avoid trailer fu*kery - implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28") + implementation("com.github.teamnewpipe:NewPipeExtractor:917554a") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance @@ -260,6 +263,8 @@ dependencies { // color palette for images -> colors implementation("androidx.palette:palette-ktx:1.0.0") + // seekbar https://github.com/rubensousa/PreviewSeekBar + implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") } tasks.register("androidSourcesJar", Jar::class) { diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index df41ef91..a84b2457 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding +import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding +import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding import com.lagradost.cloudstream3.databinding.FragmentResultBinding @@ -120,6 +122,8 @@ class ExampleInstrumentedTest { testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) + testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library) + testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library) } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15767d7b..e0d43338 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,8 @@ + + + android:supportsPictureInPicture="true" + android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer" + android:launchMode="singleTask"> @@ -175,6 +179,7 @@ @@ -184,6 +189,7 @@ android:exported="false" /> diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index a7d899b6..759f99d4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -65,6 +65,11 @@ object CommonActivity { _activity = WeakReference(value) } + @MainThread + fun setActivityInstance(newActivity: Activity?) { + activity = newActivity + } + @MainThread fun Activity?.getCastSession(): CastSession? { return (this as MainActivity?)?.mSessionManager?.currentCastSession @@ -203,23 +208,25 @@ object CommonActivity { setLocale(this, localeCode) } - fun init(act: ComponentActivity?) { - if (act == null) return - activity = act + fun init(act: Activity) { + setActivityInstance(act) + + val componentActivity = activity as? ComponentActivity ?: return + //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //https://developer.android.com/guide/topics/ui/picture-in-picture canShowPipMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT - act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN - act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS + componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN + componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS - act.updateLocale() - act.updateTv() + componentActivity.updateLocale() + componentActivity.updateTv() NewPipe.init(DownloaderTestImpl.getInstance()) for (resumeApp in resumeApps) { resumeApp.launcher = - act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val resultCode = result.resultCode val data = result.data if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { @@ -236,11 +243,11 @@ object CommonActivity { // Ask for notification permissions on Android 13 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission( - act, + componentActivity, Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { - val requestPermissionLauncher = act.registerForActivityResult( + val requestPermissionLauncher = componentActivity.registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> Log.d(TAG, "Notification permission: $isGranted") @@ -295,12 +302,15 @@ object CommonActivity { val currentOverlayTheme = when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) { "Normal" -> R.style.OverlayPrimaryColorNormal + "DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow "CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink + "Orange" -> R.style.OverlayPrimaryColorOrange "DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen "Maroon" -> R.style.OverlayPrimaryColorMaroon "NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue "Grey" -> R.style.OverlayPrimaryColorGrey "White" -> R.style.OverlayPrimaryColorWhite + "CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue "Brown" -> R.style.OverlayPrimaryColorBrown "Purple" -> R.style.OverlayPrimaryColorPurple "Green" -> R.style.OverlayPrimaryColorGreen diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 0175e0d0..5b674c4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -179,6 +179,13 @@ object APIHolder { private var trackerCache: HashMap = hashMapOf() + /** backwards compatibility, use getTracker4 instead */ + suspend fun getTracker( + titles: List, + types: Set?, + year: Int?, + ): Tracker? = getTracker(titles, types, year, false) + /** * Get anime tracker information based on title, year and type. * Both titles are attempted to be matched with both Romaji and English title. @@ -192,7 +199,7 @@ object APIHolder { titles: List, types: Set?, year: Int?, - lessAccurate: Boolean = false + lessAccurate: Boolean ): Tracker? { return try { require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a07ae2c2..4e0d93c9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint 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.databinding.ActivityMainBinding @@ -128,6 +129,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -281,6 +283,7 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" + const val ANIMATED_OUTLINE : Boolean = false var lastError: String? = null /** @@ -305,6 +308,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // kinda shitty solution, but cant com main->home otherwise for popups val bookmarksUpdatedEvent = Event() + /** + * Used by data store helper to fully reload home when switching accounts + */ + val reloadHomeEvent = Event() /** @@ -492,6 +499,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_results_phone, R.id.navigation_results_tv, R.id.navigation_player, + R.id.navigation_quick_search, ).contains(destination.id) binding?.navHostFragment?.apply { @@ -536,9 +544,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { navRailView.isVisible = isNavVisible && landscape // Hide library on TV since it is not supported yet :( - val isTrueTv = isTrueTvSettings() - navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv - navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //val isTrueTv = isTrueTvSettings() + //navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + + // Hide downloads on TV + //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } @@ -580,6 +592,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { override fun onResume() { super.onResume() afterPluginsLoadedEvent += ::onAllPluginsLoaded + setActivityInstance(this) try { if (isCastApiAvailable()) { //mCastSession = mSessionManager.currentCastSession @@ -1058,7 +1071,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - + private fun centerView(view : View?) { + if(view == null) return + try { + Log.v(TAG, "centerView: $view") + val r = Rect(0, 0, 0, 0) + view.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = r.width() / 2 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) + view.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } + } override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -1092,15 +1120,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { updateTv() // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? - try { + normalSafeApiCall { val appVer = BuildConfig.VERSION_NAME val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) - backup() + normalSafeApiCall { + backup(this) + } + normalSafeApiCall { + // Recompile oat on new version + PluginManager.deleteAllOatFiles(this) + } } - } catch (t: Throwable) { - logError(t) } // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH @@ -1108,37 +1140,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (isTvSettings()) { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) - newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> - // println("refocus $oldFocus -> $newFocus") - try { - val r = Rect(0,0,0,0) - newFocus.getDrawingRect(r) - val x = r.centerX() - val y = r.centerY() - val dx = 0 //screenWidth / 2 - val dy = screenHeight / 2 - val r2 = Rect(x-dx,y-dy,x+dx,y+dy) - newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_ : Throwable) { } - TvFocus.updateFocusView(newFocus) - /*var focus = newFocus - while(focus != null) { - if(focus is ScrollingView && focus.canScrollVertically()) { - focus.scrollBy() - } - when(focus.parent) { - is View -> focus = newFocus - else -> break - } - }*/ + if(isTrueTvSettings() && ANIMATED_OUTLINE) { + TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) + newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { + TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) + } + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + TvFocus.updateFocusView(newFocus) + } + } else { + newLocalBinding.focusOutline.isVisible = false } - newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { - TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) + + if(isTrueTvSettings()) { + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + centerView(newFocus) + } } + + ActivityMainBinding.bind(newLocalBinding.root) // this may crash } else { val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false) @@ -1182,7 +1204,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } else if (lastError == null) { ioSafe { - getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> + DataStoreHelper.currentHomePage?.let { homeApi -> mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) } ?: run { mainPluginsLoadedEvent.invoke(false) @@ -1543,6 +1565,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { migrateResumeWatching() } + getKey(USER_SELECTED_HOMEPAGE_API)?.let { homepage -> + DataStoreHelper.currentHomePage = homepage + removeKey(USER_SELECTED_HOMEPAGE_API) + } + try { if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index bcf8848c..a9fafc39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -29,7 +29,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "m4H6D9%0\$N&F6rQ&" + private const val KEY = "eN0^>\$^#M08uFv%c" } override suspend fun getUrl( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt index d76b0e11..eaf9c65f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -19,7 +19,7 @@ open class Gofile : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) + val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) @@ -59,4 +59,4 @@ open class Gofile : ExtractorApi() { @JsonProperty("data") val data: Data? = null, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt similarity index 74% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt index 213ecdf3..702501a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt @@ -7,21 +7,12 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper -class SpeedoStream2 : SpeedoStream() { - override val mainUrl = "https://speedostream.mom" -} +open class Minoplres : ExtractorApi() { -class SpeedoStream1 : SpeedoStream() { - override val mainUrl = "https://speedostream.pm" -} - -open class SpeedoStream : ExtractorApi() { - override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.bond" + override val name = "Minoplres" // formerly SpeedoStream override val requiresReferer = true - - // .bond, .pm, .mom redirect to .bond - private val hostUrl = "https://speedostream.bond" + override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond + private val hostUrl = "https://minoplres.xyz" override suspend fun getUrl(url: String, referer: String?): List { val sources = mutableListOf() 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 87b0ba3b..8e87cc99 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -137,6 +137,20 @@ object PluginManager { } } + /** + * Deletes all generated oat files which will force Android to recompile the dex extensions. + * This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update. + */ + fun deleteAllOatFiles(context: Context) { + File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo -> + repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file -> + val success = file.deleteRecursively() + Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success") + } + } + } + + fun getPluginsOnline(): Array { return getKey(PLUGINS_KEY) ?: emptyArray() } @@ -463,6 +477,14 @@ object PluginManager { Log.i(TAG, "Loading plugin: $data") return try { + /* in case of android 14 then + try { + File(filePath).setReadOnly() + } catch (t : Throwable) { + Log.e(TAG, "Failed to set dex as readonly") + logError(t) + }*/ + val loader = PathClassLoader(filePath, context.classLoader) var manifest: Plugin.Manifest loader.getResourceAsStream("manifest.json").use { stream -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt new file mode 100644 index 00000000..6ed7a447 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt @@ -0,0 +1,96 @@ +package com.lagradost.cloudstream3.services + +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import java.util.concurrent.TimeUnit + +const val BACKUP_CHANNEL_ID = "cloudstream3.backups" +const val BACKUP_WORK_NAME = "work_backup" +const val BACKUP_CHANNEL_NAME = "Backups" +const val BACKUP_CHANNEL_DESCRIPTION = "Notifications for background backups" +const val BACKUP_NOTIFICATION_ID = 938712898 // Random unique + +class BackupWorkManager(val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + companion object { + fun enqueuePeriodicWork(context: Context?, intervalHours: Long) { + if (context == null) return + + if (intervalHours == 0L) { + WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME) + return + } + + val constraints = Constraints.Builder() + .setRequiresStorageNotLow(true) + .build() + + val periodicSyncDataWork = + PeriodicWorkRequest.Builder( + BackupWorkManager::class.java, + intervalHours, + TimeUnit.HOURS + ) + .addTag(BACKUP_WORK_NAME) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + BACKUP_WORK_NAME, + ExistingPeriodicWorkPolicy.UPDATE, + periodicSyncDataWork + ) + + // Uncomment below for testing + +// val oneTimeBackupWork = +// OneTimeWorkRequest.Builder(BackupWorkManager::class.java) +// .addTag(BACKUP_WORK_NAME) +// .setConstraints(constraints) +// .build() +// +// WorkManager.getInstance(context).enqueue(oneTimeBackupWork) + } + } + + private val backupNotificationBuilder = + NotificationCompat.Builder(context, BACKUP_CHANNEL_ID) + .setColorized(true) + .setOnlyAlertOnce(true) + .setSilent(true) + .setAutoCancel(true) + .setContentTitle(context.getString(R.string.pref_category_backup)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setColor(context.colorFromAttribute(R.attr.colorPrimary)) + .setSmallIcon(R.drawable.ic_cloudstream_monochrome_big) + + override suspend fun doWork(): Result { + context.createNotificationChannel( + BACKUP_CHANNEL_ID, + BACKUP_CHANNEL_NAME, + BACKUP_CHANNEL_DESCRIPTION + ) + + setForeground( + ForegroundInfo( + BACKUP_NOTIFICATION_ID, + backupNotificationBuilder.build() + ) + ) + + BackupUtils.backup(context) + + return Result.success() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt similarity index 79% rename from app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt rename to app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 28ced48c..1a9549e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui import android.content.Context import android.util.AttributeSet import android.view.View +import androidx.core.view.children import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs @@ -70,8 +71,8 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : val orientation = this.orientation // fixes arabic by inverting left and right layout focus - val correctDirection = if(this.isLayoutRTL) { - when(direction) { + val correctDirection = if (this.isLayoutRTL) { + when (direction) { View.FOCUS_RIGHT -> View.FOCUS_LEFT View.FOCUS_LEFT -> View.FOCUS_RIGHT else -> direction @@ -83,12 +84,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return spanCount } + View.FOCUS_UP -> { return -spanCount } + View.FOCUS_RIGHT -> { return 1 } + View.FOCUS_LEFT -> { return -1 } @@ -98,12 +102,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return 1 } + View.FOCUS_UP -> { return -1 } + View.FOCUS_RIGHT -> { return spanCount } + View.FOCUS_LEFT -> { return -spanCount } @@ -155,4 +162,32 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att layoutManager = manager } +} + +/** + * Recyclerview wherein the max item width or height is set by the biggest view to prevent inconsistent view sizes. + */ +class MaxRecyclerView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) { + private var biggestObserved: Int = 0 + private val orientation = LayoutManager.getProperties(context, attrs, 0, 0).orientation + private val isHorizontal = orientation == HORIZONTAL + private fun View.updateMaxSize() { + if (isHorizontal) { + this.minimumHeight = biggestObserved + } else { + this.minimumWidth = biggestObserved + } + } + + override fun onChildAttachedToWindow(child: View) { + child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + val observed = if (isHorizontal) child.measuredHeight else child.measuredWidth + if (observed > biggestObserved) { + biggestObserved = observed + children.forEach { it.updateMaxSize() } + } else { + child.updateMaxSize() + } + super.onChildAttachedToWindow(child) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index b84c619e..4d940123 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -7,7 +7,6 @@ import android.content.DialogInterface import android.content.Intent import android.content.res.Configuration import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -23,18 +22,12 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent -import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent -import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding @@ -45,38 +38,26 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi -import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API - import java.util.* -const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list" -const val HOME_PREF_HOMEPAGE = "home_pref_homepage" - class HomeFragment : Fragment() { companion object { val configEvent = Event() @@ -378,10 +359,7 @@ class HomeFragment : Fragment() { var currentApiName = selectedApiName var currentValidApis: MutableList = mutableListOf() - val preSelectedTypes = this.getKey>(HOME_PREF_HOMEPAGE) - ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + val preSelectedTypes = DataStoreHelper.homePreference.toMutableList() binding.cancelBtt.setOnClickListener { dialog.dismissSafe() @@ -409,7 +387,7 @@ class HomeFragment : Fragment() { } fun updateList() { - this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes) + DataStoreHelper.homePreference = preSelectedTypes arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> @@ -669,7 +647,7 @@ class HomeFragment : Fragment() { } homeViewModel.reloadStored() - homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + homeViewModel.loadAndCancel(DataStoreHelper.currentHomePage, false) //loadHomePage(false) // nice profile pic on homepage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 1d8e1399..d7956f39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -461,6 +461,11 @@ class HomeParentItemAdapterPreview( } } + homePreviewSearchButton.setOnClickListener { _ -> + // Open blank screen. + viewModel.queryTextSubmit("") + } + // This makes the hidden next buttons only available when on the info button // Otherwise you might be able to go to the next item without being at the info button homePreviewInfoBtt.setOnFocusChangeListener { _, hasFocus -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index b27223ec..ad75aa9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse @@ -49,7 +48,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -171,10 +169,7 @@ class HomeViewModel : ViewModel() { currentWatchTypes.remove(WatchType.NONE) if (currentWatchTypes.size <= 0) { - setKey( - HOME_BOOKMARK_VALUE_LIST, - intArrayOf() - ) + DataStoreHelper.homeBookmarkedList = intArrayOf() _availableWatchStatusTypes.postValue(setOf() to setOf()) _bookmarks.postValue(Pair(false, ArrayList())) return@launchSafe @@ -182,16 +177,14 @@ class HomeViewModel : ViewModel() { val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) - setKey( - HOME_BOOKMARK_VALUE_LIST, - watchPrefNotNull.map { it.internalId }.toIntArray() - ) + + DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray() _availableWatchStatusTypes.postValue( - Pair( - watchPrefNotNull, - currentWatchTypes, + + watchPrefNotNull to + currentWatchTypes, + ) - ) val list = withContext(Dispatchers.IO) { watchStatusIds.filter { watchPrefNotNull.contains(it.second) } @@ -426,23 +419,29 @@ class HomeViewModel : ViewModel() { } private fun afterPluginsLoaded(forceReload: Boolean) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload) + loadAndCancel(DataStoreHelper.currentHomePage, forceReload) } private fun afterMainPluginsLoaded(unused: Boolean = false) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + loadAndCancel(DataStoreHelper.currentHomePage, false) + } + + private fun reloadHome(unused: Boolean = false) { + loadAndCancel(DataStoreHelper.currentHomePage, true) } init { MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent += ::reloadHome } override fun onCleared() { MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent -= ::reloadHome super.onCleared() } @@ -458,7 +457,7 @@ class HomeViewModel : ViewModel() { fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) - getKey(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let { + DataStoreHelper.homeBookmarkedList.map { WatchType.fromInternalId(it) }.let { list.addAll(it) } loadStoredData(list) @@ -495,7 +494,7 @@ class HomeViewModel : ViewModel() { val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + if (fromUI) DataStoreHelper.currentHomePage = noneApi.name loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed @@ -506,7 +505,7 @@ class HomeViewModel : ViewModel() { } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading @@ -520,7 +519,7 @@ class HomeViewModel : ViewModel() { } } else { // if the api is found, then set it to it and save key - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name) + if (fromUI) DataStoreHelper.currentHomePage = api.name loadAndCancel(api) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 04ef3d96..85f0aedd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -1,40 +1,56 @@ package com.lagradost.cloudstream3.ui.library +import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.os.Handler import android.os.Looper +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS +import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS import android.view.animation.AlphaAnimation +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.widget.SearchView +import androidx.core.view.allViews import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.viewpager2.widget.ViewPager2 +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA +import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount @@ -78,9 +94,21 @@ class LibraryFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val localBinding = FragmentLibraryBinding.inflate(inflater, container, false) - binding = localBinding - return localBinding.root + val layout = + if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library + val root = inflater.inflate(layout, container, false) + binding = try { + FragmentLibraryBinding.bind(root) + } catch (t: Throwable) { + CommonActivity.showToast( + txt(R.string.unable_to_inflate, t.message ?: ""), + Toast.LENGTH_LONG + ) + logError(t) + null + } + + return root //return inflater.inflate(R.layout.fragment_library, container, false) } @@ -97,24 +125,16 @@ class LibraryFragment : Fragment() { super.onSaveInstanceState(outState) } + @SuppressLint("ResourceType", "CutPasteId") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fixPaddingStatusbar(binding?.searchStatusBarPadding) - binding?.sortFab?.setOnClickListener { - val methods = libraryViewModel.sortingMethods.map { - txt(it.stringRes).asString(view.context) - } + binding?.sortFab?.setOnClickListener(sortChangeClickListener) + binding?.librarySort?.setOnClickListener(sortChangeClickListener) - activity?.showBottomDialog(methods, - libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), - txt(R.string.sort_by).asString(view.context), - false, - {}, - { - val method = libraryViewModel.sortingMethods[it] - libraryViewModel.sort(method) - }) + binding?.libraryRoot?.findViewById(R.id.search_src_text)?.apply { + tag = "tv_no_focus_tag" } binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { @@ -180,7 +200,7 @@ class LibraryFragment : Fragment() { val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders - val savedSelection = getKey(LIBRARY_FOLDER, key) + val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", key) val selectedIndex = when { savedSelection == null -> 0 @@ -215,7 +235,7 @@ class LibraryFragment : Fragment() { } setKey( - LIBRARY_FOLDER, + "$currentAccount/$LIBRARY_FOLDER", key, savedData, ) @@ -228,6 +248,7 @@ class LibraryFragment : Fragment() { } binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) + binding?.viewpager?.adapter = binding?.viewpager?.adapter ?: ViewpagerAdapter( mutableListOf(), @@ -262,8 +283,11 @@ class LibraryFragment : Fragment() { // This basically first selects the individual opener and if that is default then // selects the whole list opener val savedListSelection = - getKey(LIBRARY_FOLDER, syncName.name) - val savedSelection = getKey(LIBRARY_FOLDER, syncId).takeIf { + getKey("$currentAccount/$LIBRARY_FOLDER", syncName.name) + val savedSelection = getKey( + "$currentAccount/$LIBRARY_FOLDER", + syncId + ).takeIf { it?.openType != LibraryOpenerType.Default } ?: savedListSelection @@ -351,11 +375,18 @@ class LibraryFragment : Fragment() { } (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + //fix focus on the viewpager itself + (viewpager.getChildAt(0) as RecyclerView).apply { + tag = "tv_no_focus_tag" + //isFocusable = false + } + // Using notifyItemRangeChanged keeps the animations when sorting viewpager.adapter?.notifyItemRangeChanged( 0, viewpager.adapter?.itemCount ?: 0 ) + binding?.viewpager?.setCurrentItem(libraryViewModel.currentPage, false) // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating // Without this there would be a flashing effect: @@ -392,6 +423,9 @@ class LibraryFragment : Fragment() { viewpager, ) { tab, position -> tab.text = pages.getOrNull(position)?.title?.asStringNull(context) + tab.view.tag = "tv_no_focus_tag" + tab.view.nextFocusDownId = R.id.search_result_root + tab.view.setOnClickListener { val currentItem = binding?.viewpager?.currentItem ?: return@setOnClickListener @@ -414,12 +448,45 @@ class LibraryFragment : Fragment() { } } } - } + binding?.viewpager?.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + val all = binding?.viewpager?.allViews?.toList() + ?.filterIsInstance() + all?.forEach { view -> + view.isVisible = view.tag == position + view.isFocusable = view.tag == position + + if (view.tag == position) + view.descendantFocusability = FOCUS_AFTER_DESCENDANTS + else + view.descendantFocusability = FOCUS_BLOCK_DESCENDANTS + } + super.onPageSelected(position) + } + }) + } override fun onConfigurationChanged(newConfig: Configuration) { (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() super.onConfigurationChanged(newConfig) } + + private val sortChangeClickListener = View.OnClickListener { view -> + val methods = libraryViewModel.sortingMethods.map { + txt(it.stringRes).asString(view.context) + } + + activity?.showBottomDialog(methods, + libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), + txt(R.string.sort_by).asString(view.context), + false, + {}, + { + val method = libraryViewModel.sortingMethods[it] + libraryViewModel.sort(method) + }) + } } class MenuSearchView(context: Context) : SearchView(context) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 14d31356..b44913d9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -6,11 +6,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount enum class ListSorting(@StringRes val stringRes: Int) { Query(R.string.none), @@ -28,6 +31,8 @@ class LibraryViewModel : ViewModel() { private val _pages: MutableLiveData>> = MutableLiveData(null) val pages: LiveData>> = _pages + var currentPage: Int = 0 + private val _currentApiName: MutableLiveData = MutableLiveData("") val currentApiName: LiveData = _currentApiName @@ -35,12 +40,12 @@ class LibraryViewModel : ViewModel() { get() = SyncApis.filter { it.hasAccount() } var currentSyncApi = availableSyncApis.let { allApis -> - val lastSelection = getKey(LAST_SYNC_API_KEY) + val lastSelection = getKey("$currentAccount/$LAST_SYNC_API_KEY") availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull() } private set(value) { field = value - setKey(LAST_SYNC_API_KEY, field?.name) + setKey("$currentAccount/$LAST_SYNC_API_KEY", field?.name) } val availableApiNames: List @@ -58,13 +63,21 @@ class LibraryViewModel : ViewModel() { reloadPages(true) } - fun sort(method: ListSorting, query: String? = null) { - val currentList = pages.value ?: return + fun sort(method: ListSorting, query: String? = null) = ioSafe { + val value = _pages.value ?: return@ioSafe + if (value is Resource.Success) { + sort(method, query, value.value) + } + } + + private fun sort(method: ListSorting, query: String? = null, items: List) { currentSortingMethod = method - (currentList as? Resource.Success)?.value?.forEachIndexed { _, page -> + DataStoreHelper.librarySortingMode = method.ordinal + + items.forEach { page -> page.sort(method, query) } - _pages.postValue(currentList) + _pages.postValue(Resource.Success(items)) } fun reloadPages(forceReload: Boolean) { @@ -85,8 +98,6 @@ class LibraryViewModel : ViewModel() { val library = (libraryResource as? Resource.Success)?.value ?: return@let sortingMethods = library.supportedListSorting.toList() - currentSortingMethod = null - repo.requireLibraryRefresh = false val pages = library.allLibraryLists.map { @@ -96,8 +107,24 @@ class LibraryViewModel : ViewModel() { ) } - _pages.postValue(Resource.Success(pages)) + val desiredSortingMethod = + ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) + if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { + sort(desiredSortingMethod, null, pages) + } else { + // null query = no sorting + sort(ListSorting.Query, null, pages) + } } } } + + init { + MainActivity.reloadHomeEvent += ::reloadPages + } + + override fun onCleared() { + MainActivity.reloadHomeEvent -= ::reloadPages + super.onCleared() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index 95fefcbe..76028487 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -25,7 +25,7 @@ class ViewpagerAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is PageViewHolder -> { - holder.bind(pages[position], unbound.remove(position)) + holder.bind(pages[position], position, unbound.remove(position)) } } } @@ -43,7 +43,8 @@ class ViewpagerAdapter( inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(page: SyncAPI.Page, rebind: Boolean) { + fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { + binding.pageRecyclerview.tag = position binding.pageRecyclerview.apply { spanCount = this@PageViewHolder.itemView.context.getSpanCount() ?: 3 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 4316bbc6..17a90da9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -18,10 +18,9 @@ import android.widget.ProgressBar import android.widget.Toast import androidx.annotation.LayoutRes import androidx.annotation.StringRes +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.preference.PreferenceManager -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession @@ -30,12 +29,15 @@ import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView import androidx.media3.ui.TimeBar -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import androidx.preference.PreferenceManager +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat +import com.github.rubensousa.previewseekbar.PreviewBar +import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode import com.lagradost.cloudstream3.CommonActivity.isInPIPMode import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener +import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError @@ -45,6 +47,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI @@ -77,12 +80,12 @@ abstract class AbstractPlayerFragment( var isBuffering = true protected open var hasPipModeSupport = true - var playerPausePlayHolderHolder : FrameLayout? = null - var playerPausePlay : ImageView? = null - var playerBuffering : ProgressBar? = null - var playerView : PlayerView? = null - var piphide : FrameLayout? = null - var subtitleHolder : FrameLayout? = null + var playerPausePlayHolderHolder: FrameLayout? = null + var playerPausePlay: ImageView? = null + var playerBuffering: ProgressBar? = null + var playerView: PlayerView? = null + var piphide: FrameLayout? = null + var subtitleHolder: FrameLayout? = null @LayoutRes protected open var layout: Int = R.layout.fragment_player @@ -95,11 +98,11 @@ abstract class AbstractPlayerFragment( throw NotImplementedError() } - open fun playerPositionChanged(position: Long, duration : Long) { + open fun playerPositionChanged(position: Long, duration: Long) { throw NotImplementedError() } - open fun playerDimensionsLoaded(width: Int, height : Int) { + open fun playerDimensionsLoaded(width: Int, height: Int) { throw NotImplementedError() } @@ -135,8 +138,10 @@ abstract class AbstractPlayerFragment( } } - private fun updateIsPlaying(wasPlaying : CSPlayerLoading, - isPlaying : CSPlayerLoading) { + private fun updateIsPlaying( + wasPlaying: CSPlayerLoading, + isPlaying: CSPlayerLoading + ) { val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying @@ -184,7 +189,11 @@ abstract class AbstractPlayerFragment( canEnterPipMode = isPlayingRightNow && hasPipModeSupport if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { activity?.let { act -> - PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow, player.getAspectRatio()) + PlayerPipHelper.updatePIPModeActions( + act, + isPlayingRightNow, + player.getAspectRatio() + ) } } } @@ -373,49 +382,61 @@ abstract class AbstractPlayerFragment( /** This receives the events from the player, if you want to append functionality you do it here, * do note that this only receives events for UI changes, * and returning early WONT stop it from changing in eg the player time or pause status */ - open fun mainCallback(event : PlayerEvent) { + open fun mainCallback(event: PlayerEvent) { Log.i(TAG, "Handle event: $event") - when(event) { + when (event) { is ResizedEvent -> { playerDimensionsLoaded(event.width, event.height) } + is PlayerAttachedEvent -> { playerUpdated(event.player) } + is SubtitlesUpdatedEvent -> { subtitlesChanged() } + is TimestampSkippedEvent -> { onTimestampSkipped(event.timestamp) } + is TimestampInvokedEvent -> { onTimestamp(event.timestamp) } + is TracksChangedEvent -> { onTracksInfoChanged() } + is EmbeddedSubtitlesFetchedEvent -> { embeddedSubtitlesFetched(event.tracks) } + is ErrorEvent -> { playerError(event.error) } + is RequestAudioFocusEvent -> { requestAudioFocus() } + is EpisodeSeekEvent -> { - when(event.offset) { + when (event.offset) { -1 -> prevEpisode() 1 -> nextEpisode() else -> {} } } + is StatusEvent -> { updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying) } + is PositionEvent -> { playerPositionChanged(position = event.toMs, duration = event.durationMs) } + is VideoEndedEvent -> { context?.let { ctx -> // Only play next episode if autoplay is on (default) @@ -432,6 +453,7 @@ abstract class AbstractPlayerFragment( } } } + is PauseEvent -> Unit is PlayEvent -> Unit } @@ -439,7 +461,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 + resizeMode = DataStoreHelper.resizeMode resize(resizeMode, false) player.releaseCallbacks() @@ -454,22 +476,73 @@ abstract class AbstractPlayerFragment( ) if (player is CS3IPlayer) { + // preview bar + val progressBar: PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress) + val previewImageView: ImageView? = playerView?.findViewById(R.id.previewImageView) + val previewFrameLayout: FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout) + if (progressBar != null && previewImageView != null && previewFrameLayout != null) { + var resume = false + progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener { + override fun onScrubStart(previewBar: PreviewBar?) { + val hasPreview = player.hasPreview() + progressBar.isPreviewEnabled = hasPreview + resume = player.getIsPlaying() + if (resume) player.handleEvent( + CSPlayerEvent.Pause, + PlayerEventSource.Player + ) + } + + override fun onScrubMove( + previewBar: PreviewBar?, + progress: Int, + fromUser: Boolean + ) { + } + + override fun onScrubStop(previewBar: PreviewBar?) { + if (resume) player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.Player) + } + }) + progressBar.attachPreviewView(previewFrameLayout) + progressBar.setPreviewLoader { currentPosition, max -> + val bitmap = player.getPreview(currentPosition.toFloat().div(max.toFloat())) + previewImageView.isGone = bitmap == null + previewImageView.setImageBitmap(bitmap) + } + } + subView = playerView?.findViewById(R.id.exo_subtitles) subStyle = SubtitlesFragment.getCurrentSavedStyle() player.initSubtitles(subView, subtitleHolder, subStyle) + (player.imageGenerator as? PreviewGenerator)?.params = ImageParams.new16by9(screenWidth) + /*previewImageView?.doOnLayout { + (player.imageGenerator as? PreviewGenerator)?.params = ImageParams( + it.measuredWidth, + it.measuredHeight + ) + }*/ /** this might seam a bit fucky and that is because it is, the seek event is captured twice, once by the player * and once by the UI even if it should only be registered once by the UI */ - playerView?.findViewById(R.id.exo_progress)?.addListener(object : TimeBar.OnScrubListener { - override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit - override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit - override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { - if (canceled) return - val playerDuration = player.getDuration() ?: return - val playerPosition = player.getPosition() ?: return - mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position)) - } - }) + playerView?.findViewById(R.id.exo_progress) + ?.addListener(object : TimeBar.OnScrubListener { + override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit + override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit + override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { + if (canceled) return + val playerDuration = player.getDuration() ?: return + val playerPosition = player.getPosition() ?: return + mainCallback( + PositionEvent( + source = PlayerEventSource.UI, + durationMs = playerDuration, + fromMs = playerPosition, + toMs = position + ) + ) + } + }) SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged @@ -517,6 +590,7 @@ abstract class AbstractPlayerFragment( canEnterPipMode = false mMediaSession?.release() mMediaSession = null + playerView?.player = null SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) @@ -534,7 +608,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { - setKey(RESIZE_MODE_KEY, resize.ordinal) + DataStoreHelper.resizeMode = resize.ordinal val type = when (resize) { PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index fe4e3423..8d1eb7df 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.net.Uri import android.os.Handler import android.os.Looper @@ -51,10 +52,12 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink @@ -85,10 +88,20 @@ const val toleranceAfterUs = 300_000L class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null + set(value) { + // If the old value is not null then the player has not been properly released. + debugAssert( + { field != null && value != null }, + { "Previous player instance should be released!" }) + field = value + } + var cacheSize = 0L var simpleCacheSize = 0L var videoBufferMs = 0L + val imageGenerator = IPreviewGenerator.new() + private val seekActionTime = 30000L private var ignoreSSL: Boolean = true @@ -175,6 +188,14 @@ class CS3IPlayer : IPlayer { subtitleHelper.initSubtitles(subView, subHolder, style) } + override fun getPreview(fraction: Float): Bitmap? { + return imageGenerator.getPreviewImage(fraction) + } + + override fun hasPreview(): Boolean { + return imageGenerator.hasPreview() + } + override fun loadPlayer( context: Context, sameEpisode: Boolean, @@ -183,7 +204,8 @@ class CS3IPlayer : IPlayer { startPosition: Long?, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? + autoPlay: Boolean?, + preview: Boolean, ) { Log.i(TAG, "loadPlayer") if (sameEpisode) { @@ -202,11 +224,30 @@ class CS3IPlayer : IPlayer { // release the current exoplayer and cache releasePlayer() + if (link != null) { + // only video support atm + (imageGenerator as? PreviewGenerator)?.let { gen -> + if (preview) { + gen.load(link, sameEpisode) + } else { + gen.clear(sameEpisode) + } + } loadOnlinePlayer(context, link) } else if (data != null) { + (imageGenerator as? PreviewGenerator)?.let { gen -> + if (preview) { + gen.load(context, data, sameEpisode) + } else { + gen.clear(sameEpisode) + } + } loadOfflinePlayer(context, data) + } else { + throw IllegalArgumentException("Requires link or uri") } + } override fun setActiveSubtitles(subtitles: Set) { @@ -487,6 +528,7 @@ class CS3IPlayer : IPlayer { } override fun release() { + imageGenerator.release() releasePlayer() } @@ -501,12 +543,15 @@ class CS3IPlayer : IPlayer { **/ var preferredAudioTrackLanguage: String? = null get() { - return field ?: getKey(PREFERRED_AUDIO_LANGUAGE_KEY, field)?.also { + return field ?: getKey( + "$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", + field + )?.also { field = it } } set(value) { - setKey(PREFERRED_AUDIO_LANGUAGE_KEY, value) + setKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", value) field = value } @@ -682,13 +727,13 @@ class CS3IPlayer : IPlayer { metadataRendererOutput ).map { if (it is TextRenderer) { - currentTextRenderer = CustomTextRenderer( + val currentTextRenderer = CustomTextRenderer( subtitleOffset, textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ) - currentTextRenderer!! + ).also { this.currentTextRenderer = it } + currentTextRenderer } else it }.toTypedArray() } @@ -864,8 +909,20 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) - CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source)) - CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source)) + CSPlayerEvent.NextEpisode -> event( + EpisodeSeekEvent( + offset = 1, + source = source + ) + ) + + CSPlayerEvent.PrevEpisode -> event( + EpisodeSeekEvent( + offset = -1, + source = source + ) + ) + CSPlayerEvent.SkipCurrentChapter -> { //val dur = this@CS3IPlayer.getDuration() ?: return@apply getCurrentTimestamp()?.let { lastTimeStamp -> @@ -1323,7 +1380,7 @@ class CS3IPlayer : IPlayer { override fun reloadPlayer(context: Context) { Log.i(TAG, "reloadPlayer") - exoPlayer?.release() + releasePlayer(false) currentLink?.let { loadOnlinePlayer(context, it) } ?: currentDownloadedFile?.let { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index d181e175..4c3376bb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -110,4 +110,9 @@ class DownloadedPlayerActivity : AppCompatActivity() { return } } + + override fun onResume() { + super.onResume() + CommonActivity.setActivityInstance(this) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index e698191d..819e50ba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -356,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setPlayBackSpeed(speed: Float) { try { - setKey(PLAYBACK_SPEED_KEY, speed) + DataStoreHelper.playBackSpeed = speed playerBinding?.playerSpeedBtt?.text = getString(R.string.player_speed_text_format).format(speed) .replace(".0x", "x") @@ -1194,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // init variables - setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f) + setPlayBackSpeed(DataStoreHelper.playBackSpeed) savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let { subtitleDelay = it } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index b2542ffa..1c751897 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -180,6 +180,7 @@ class GeneratorPlayer : FullScreenPlayer() { (if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle( currentSubs, settings = true, downloads = true ), + preview = isFullScreenPlayer ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index ec006234..0e54e2cb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context +import android.graphics.Bitmap import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip @@ -198,17 +199,8 @@ data class CurrentTracks( class InvalidFileException(msg: String) : Exception(msg) //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -const val STATE_RESUME_WINDOW = "resumeWindow" -const val STATE_RESUME_POSITION = "resumePosition" -const val STATE_PLAYER_FULLSCREEN = "playerFullscreen" -const val STATE_PLAYER_PLAYING = "playerOnPlay" const val ACTION_MEDIA_CONTROL = "media_control" const val EXTRA_CONTROL_TYPE = "control_type" -const val PLAYBACK_SPEED = "playback_speed" -const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode -const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed -const val PREFERRED_SUBS_KEY = "preferred_subtitles" // Last used resize mode -//const val PLAYBACK_FASTFORWARD = "playback_fastforward" // Last used resize mode /** Abstract Exoplayer logic, can be expanded to other players */ interface IPlayer { @@ -246,11 +238,15 @@ interface IPlayer { startPosition: Long? = null, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? = true + autoPlay: Boolean? = true, + preview : Boolean = true, ) fun reloadPlayer(context: Context) + fun getPreview(fraction : Float) : Bitmap? + fun hasPreview() : Boolean + fun setActiveSubtitles(subtitles: Set) fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun getCurrentPreferredSubtitle(): SubtitleData? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 42659f8d..3179cb9f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -15,6 +16,7 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorUri import kotlinx.coroutines.Job +import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { @@ -38,6 +40,11 @@ class PlayerGeneratorViewModel : ViewModel() { private val _currentSubtitleYear = MutableLiveData(null) val currentSubtitleYear: LiveData = _currentSubtitleYear + /** + * Save the Episode ID to prevent starting multiple link loading Jobs when preloading links. + */ + private var currentLoadingEpisodeId: Int? = null + fun setSubtitleYear(year: Int?) { _currentSubtitleYear.postValue(year) } @@ -72,18 +79,32 @@ class PlayerGeneratorViewModel : ViewModel() { } fun preLoadNextLinks() { + val id = getId() + // Do not preload if already loading + if (id == currentLoadingEpisodeId) return + Log.i(TAG, "preLoadNextLinks") currentJob?.cancel() - currentJob = viewModelScope.launchSafe { - if (generator?.hasCache == true && generator?.hasNext() == true) { - safeApiCall { - generator?.generateLinks( - type = LoadType.InApp, - clearCache = false, - callback = {}, - subtitleCallback = {}, - offset = 1 - ) + currentLoadingEpisodeId = id + + currentJob = viewModelScope.launch { + try { + if (generator?.hasCache == true && generator?.hasNext() == true) { + safeApiCall { + generator?.generateLinks( + type = LoadType.InApp, + clearCache = false, + callback = {}, + subtitleCallback = {}, + offset = 1 + ) + } + } + } catch (t: Throwable) { + logError(t) + } finally { + if (currentLoadingEpisodeId == id) { + currentLoadingEpisodeId = null } } } @@ -162,14 +183,14 @@ class PlayerGeneratorViewModel : ViewModel() { // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { - generator?.generateLinks(type = type,clearCache = clearCache, callback = { + generator?.generateLinks(type = type, clearCache = clearCache, callback = { currentLinks.add(it) // Clone to prevent ConcurrentModificationException normalSafeApiCall { // Extra normalSafeApiCall since .toSet() iterates. _currentLinks.postValue(currentLinks.toSet()) } - }, subtitleCallback = { + }, subtitleCallback = { currentSubs.add(it) normalSafeApiCall { _currentSubs.postValue(currentSubs.toSet()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt new file mode 100644 index 00000000..6414374b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -0,0 +1,541 @@ +package com.lagradost.cloudstream3.ui.player + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Build +import android.util.Log +import androidx.annotation.WorkerThread +import androidx.core.graphics.scale +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.M3u8Helper2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import kotlin.math.absoluteValue +import kotlin.math.ceil +import kotlin.math.log2 + +const val MAX_LOD = 6 +const val MIN_LOD = 3 + +data class ImageParams( + val width: Int, + val height: Int, +) { + companion object { + val DEFAULT = ImageParams(200, 320) + fun new16by9(width: Int): ImageParams { + if (width < 100) { + return DEFAULT + } + return ImageParams( + width / 4, + (width * 9) / (4 * 16) + ) + } + } + + init { + assert(width > 0 && height > 0) + } +} + +interface IPreviewGenerator { + fun hasPreview(): Boolean + fun getPreviewImage(fraction: Float): Bitmap? + fun release() + + var params: ImageParams + + var durationMs: Long + var loadedImages: Int + + companion object { + fun new(): IPreviewGenerator { + /** because TV has low ram + not show we disable this for now */ + return if (SettingsFragment.isTrueTvSettings()) { + empty() + } else { + PreviewGenerator() + } + } + + fun empty(): IPreviewGenerator { + return NoPreviewGenerator() + } + } +} + +private fun rescale(image: Bitmap, params: ImageParams): Bitmap { + if (image.width <= params.width && image.height <= params.height) return image + val new = image.scale(params.width, params.height) + // throw away the old image + if (new != image) { + image.recycle() + } + return new +} + +/** rescale to not take up as much memory */ +private fun MediaMetadataRetriever.image(timeUs: Long, params: ImageParams): Bitmap? { + /*if (timeUs <= 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + val primary = this.primaryImage + if (primary != null) { + return rescale(primary, params) + } + } catch (t: Throwable) { + logError(t) + } + }*/ + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + this.getScaledFrameAtTime( + timeUs, + MediaMetadataRetriever.OPTION_CLOSEST_SYNC, + params.width, + params.height + ) + } else { + return rescale(this.getFrameAtTime(timeUs) ?: return null, params) + } +} + +/** PreviewGenerator that hides the implementation details of the sub generators that is used, used for source switch cache */ +class PreviewGenerator : IPreviewGenerator { + + /** the most up to date generator, will always mirror the actual source in the player */ + private var currentGenerator: IPreviewGenerator = NoPreviewGenerator() + + /** the longest generated preview of the same episode */ + private var lastGenerator: IPreviewGenerator = NoPreviewGenerator() + + /** always NoPreviewGenerator, used as a cache for nothing */ + private val dummy: IPreviewGenerator = NoPreviewGenerator() + + /** if the current generator is the same as the last by checking time */ + private fun isSameLength(): Boolean = + currentGenerator.durationMs.minus(lastGenerator.durationMs).absoluteValue < 10_000L + + /** use the backup if the current generator is init or if they have the same length */ + private val backupGenerator: IPreviewGenerator + get() { + if (currentGenerator.durationMs == 0L || isSameLength()) { + return lastGenerator + } + return dummy + } + + override fun hasPreview(): Boolean { + return currentGenerator.hasPreview() || backupGenerator.hasPreview() + } + + override fun getPreviewImage(fraction: Float): Bitmap? { + return try { + currentGenerator.getPreviewImage(fraction) ?: backupGenerator.getPreviewImage(fraction) + } catch (t: Throwable) { + logError(t) + null + } + } + + override fun release() { + lastGenerator.release() + currentGenerator.release() + lastGenerator = NoPreviewGenerator() + currentGenerator = NoPreviewGenerator() + } + + override var params: ImageParams = ImageParams.DEFAULT + set(value) { + field = value + lastGenerator.params = value + backupGenerator.params = value + currentGenerator.params = value + } + + override var durationMs: Long + get() = currentGenerator.durationMs + set(_) {} + override var loadedImages: Int + get() = currentGenerator.loadedImages + set(_) {} + + fun clear(keepCache: Boolean) { + if (keepCache) { + if (!isSameLength() || currentGenerator.loadedImages >= lastGenerator.loadedImages || lastGenerator.durationMs == 0L) { + // the current generator is better than the last generator, therefore keep the current + // or the lengths are not the same, therefore favoring the more recent selection + + // if they are the same we favor the current generator + lastGenerator.release() + lastGenerator = currentGenerator + } else { + // otherwise just keep the last generator and throw away the current generator + currentGenerator.release() + } + } else { + // we switched the episode, therefore keep nothing + lastGenerator.release() + lastGenerator = NoPreviewGenerator() + currentGenerator.release() + // we assume that we set currentGenerator right after this, so currentGenerator != NoPreviewGenerator + } + } + + fun load(link: ExtractorLink, keepCache: Boolean) { + clear(keepCache) + + when (link.type) { + ExtractorLinkType.M3U8 -> { + currentGenerator = M3u8PreviewGenerator(params).apply { + load(url = link.url, headers = link.getAllHeaders()) + } + } + + ExtractorLinkType.VIDEO -> { + currentGenerator = Mp4PreviewGenerator(params).apply { + load(url = link.url, headers = link.getAllHeaders()) + } + } + + else -> { + Log.i("PreviewImg", "unsupported format for $link") + } + } + } + + fun load(context: Context, link: ExtractorUri, keepCache: Boolean) { + clear(keepCache) + currentGenerator = Mp4PreviewGenerator(params).apply { + load(keepCache = keepCache, context = context, uri = link.uri) + } + } +} + +@Suppress("UNUSED_PARAMETER") +private class NoPreviewGenerator : IPreviewGenerator { + override fun hasPreview(): Boolean = false + override fun getPreviewImage(fraction: Float): Bitmap? = null + override fun release() = Unit + override var params: ImageParams + get() = ImageParams(0, 0) + set(value) {} + override var durationMs: Long = 0L + override var loadedImages: Int = 0 +} + +private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewGenerator { + // generated images 1:1 to idx of hsl + private var images: Array = arrayOf() + + private val TAG = "PreviewImgM3u8" + + // prefixSum[i] = sum(hsl.ts[0..i].time) + // where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b + private var prefixSum: Array = arrayOf() + + // how many images has been generated + override var loadedImages: Int = 0 + + // how many images we can generate in total, == hsl.size ?: 0 + private var totalImages: Int = 0 + + override fun hasPreview(): Boolean { + return totalImages > 0 && loadedImages >= minOf(totalImages, 4) + } + + override fun getPreviewImage(fraction: Float): Bitmap? { + var bestIdx = -1 + var bestDiff = Double.MAX_VALUE + synchronized(images) { + // just find the best one in a for loop, we don't care about bin searching rn + for (i in 0..images.size) { + val diff = prefixSum[i].minus(fraction).absoluteValue + if (diff > bestDiff) { + break + } + if (images[i] != null) { + bestIdx = i + bestDiff = diff + } + } + return images.getOrNull(bestIdx) + } + /* + val targetIndex = prefixSum.binarySearch(target) + var ret = images[targetIndex] + if (ret != null) { + return ret + } + for (i in 0..images.size) { + ret = images.getOrNull(i+targetIndex) ?: + }*/ + } + + private fun clear() { + synchronized(images) { + currentJob?.cancel() + // for (i in images.indices) { + // images[i]?.recycle() + // } + images = arrayOf() + prefixSum = arrayOf() + loadedImages = 0 + totalImages = 0 + } + } + + override fun release() { + clear() + images = arrayOf() + } + + override var durationMs: Long = 0L + + private var currentJob: Job? = null + fun load(url: String, headers: Map) { + clear() + currentJob?.cancel() + currentJob = ioSafe { + withContext(Dispatchers.IO) { + Log.i(TAG, "Loading with url = $url headers = $headers") + //tmpFile = + // File.createTempFile("video", ".ts", context.cacheDir).apply { + // deleteOnExit() + // } + val retriever = MediaMetadataRetriever() + val hsl = M3u8Helper2.hslLazy( + listOf( + M3u8Helper.M3u8Stream( + streamUrl = url, + headers = headers + ) + ), + selectBest = false + ) + + // no support for encryption atm + if (hsl.isEncrypted) { + Log.i(TAG, "m3u8 is encrypted") + totalImages = 0 + return@withContext + } + + // total duration of the entire m3u8 in seconds + val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 } + durationMs = (duration * 1000.0).toLong() + val durationInv = 1.0 / duration + + // if the total duration is less then 10s then something is very wrong or + // too short playback to matter + if (duration <= 10.0) { + totalImages = 0 + return@withContext + } + + totalImages = hsl.allTsLinks.size + + // we cant init directly as it is no guarantee of in order + prefixSum = Array(hsl.allTsLinks.size + 1) { 0.0 } + var runningSum = 0.0 + for (i in hsl.allTsLinks.indices) { + runningSum += (hsl.allTsLinks[i].time ?: 0.0) + prefixSum[i + 1] = runningSum * durationInv + } + synchronized(images) { + images = Array(hsl.size) { null } + loadedImages = 0 + } + + val maxLod = ceil(log2(duration)).toInt().coerceIn(MIN_LOD, MAX_LOD) + val count = hsl.allTsLinks.size + for (l in 1..maxLod) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val index = (count.div(1 shl l) + (i * count) / items).coerceIn(0, hsl.size) + if (synchronized(images) { images[index] } != null) { + continue + } + Log.i(TAG, "Generating preview for $index") + + val ts = hsl.allTsLinks[index] + try { + retriever.setDataSource(ts.url, hsl.headers) + if (!isActive) { + return@withContext + } + val img = retriever.image(0, params) + if (!isActive) { + return@withContext + } + if (img == null || img.width <= 1 || img.height <= 1) continue + synchronized(images) { + images[index] = img + loadedImages += 1 + } + } catch (t: Throwable) { + logError(t) + continue + } + + /* + val buffer = hsl.resolveLinkSafe(index) ?: continue + tmpFile?.writeBytes(buffer) + val buff = FileOutputStream(tmpFile) + retriever.setDataSource(buff.fd) + val frame = retriever.getFrameAtTime(0L)*/ + } + } + + } + } + } +} + +private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGenerator { + // lod = level of detail where the number indicates how many ones there is + // 2^(lod-1) = images + private var loadedLod = 0 + override var loadedImages = 0 + private var images = Array((1 shl MAX_LOD) - 1) { + null + } + + override fun hasPreview(): Boolean { + synchronized(images) { + return loadedLod >= MIN_LOD + } + } + + val TAG = "PreviewImgMp4" + + override fun getPreviewImage(fraction: Float): Bitmap? { + synchronized(images) { + if (loadedLod < MIN_LOD) { + Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD") + return null + } + Log.i(TAG, "Requesting preview for $fraction") + + var bestIdx = 0 + var bestDiff = 0.5f.minus(fraction).absoluteValue + + // this should be done mathematically, but for now we just loop all images + for (l in 1..loadedLod + 1) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val idx = items - 1 + i + if (idx > loadedImages) { + break + } + if (images[idx] == null) { + continue + } + val currentFraction = + (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) + val diff = currentFraction.minus(fraction).absoluteValue + if (diff < bestDiff) { + bestDiff = diff + bestIdx = idx + } + } + } + Log.i(TAG, "Best diff found at ${bestDiff * 100}% diff (${bestIdx})") + return images[bestIdx] + } + } + + // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever + private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() + + private fun clear(keepCache: Boolean) { + if (keepCache) return + synchronized(images) { + loadedLod = 0 + loadedImages = 0 + // for (i in images.indices) { + // images[i]?.recycle() + // images[i] = null + //} + images.fill(null) + } + } + + private var currentJob: Job? = null + fun load(url: String, headers: Map) { + currentJob?.cancel() + currentJob = ioSafe { + Log.i(TAG, "Loading with url = $url headers = $headers") + clear(true) + retriever.setDataSource(url, headers) + start(this) + } + } + + fun load(keepCache: Boolean, context: Context, uri: Uri) { + currentJob?.cancel() + currentJob = ioSafe { + Log.i(TAG, "Loading with uri = $uri") + clear(keepCache) + retriever.setDataSource(context, uri) + start(this) + } + } + + override fun release() { + currentJob?.cancel() + clear(false) + } + + override var durationMs: Long = 0L + + @Throws + @WorkerThread + private fun start(scope: CoroutineScope) { + Log.i(TAG, "Started loading preview") + + val durationMs = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() + ?: throw IllegalArgumentException("Bad video duration") + this.durationMs = durationMs + val durationUs = (durationMs * 1000L).toFloat() + //val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: throw IllegalArgumentException("Bad video width") + //val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: throw IllegalArgumentException("Bad video height") + + // log2 # 10s durations in the video ~= how many segments we have + val maxLod = ceil(log2((durationMs / 10_000).toFloat())).toInt().coerceIn(MIN_LOD, MAX_LOD) + + for (l in 1..maxLod) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val idx = items - 1 + i // as sum(prev) = cur-1 + // frame = 100 / 2^lod + i * 100 / 2^(lod-1) = duration % where lod is one indexed + val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) + Log.i(TAG, "Generating preview for ${fraction * 100}%") + val frame = durationUs * fraction + val img = retriever.image(frame.toLong(), params); + if (!scope.isActive) return + if (img == null || img.width <= 1 || img.height <= 1) continue + synchronized(images) { + images[idx] = img + loadedImages = maxOf(loadedImages, idx) + } + } + + synchronized(images) { + loadedLod = maxOf(loadedLod, l) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 89a09ae2..53c7c2fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -269,6 +270,10 @@ class QuickSearchFragment : Fragment() { activity?.popCurrentPage() } + if (isTrueTvSettings()) { + binding?.quickSearch?.requestFocus() + } + arguments?.getString(AUTOSEARCH_KEY)?.let { binding?.quickSearch?.setQuery(it, true) arguments?.remove(AUTOSEARCH_KEY) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 531cb5d2..7b743388 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -12,7 +14,10 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter() { +class ActorAdaptor( + private var nextFocusUpId: Int? = null, + private val focusCallback: (View?) -> Unit = {} +) : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -22,7 +27,8 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + focusCallback ) } @@ -64,10 +70,10 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV } } - private class CardViewHolder + private inner class CardViewHolder constructor( val binding: CastItemBinding, - private val focusCallback : (View?) -> Unit = {} + private val focusCallback: (View?) -> Unit = {} ) : RecyclerView.ViewHolder(binding.root) { @@ -78,8 +84,18 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV Pair(actor.voiceActor?.image, actor.actor.image) } + // Fix tv focus escaping the recyclerview + if (position == 0) { + itemView.nextFocusLeftId = R.id.result_cast_items + } else if ((position - 1) == itemCount) { + itemView.nextFocusRightId = R.id.result_cast_items + } + nextFocusUpId?.let { + itemView.nextFocusUpId = it + } + itemView.setOnFocusChangeListener { v, hasFocus -> - if(hasFocus) { + if (hasFocus) { focusCallback(v) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index ef2ed0df..e5f16dd5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -151,7 +151,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { startPosition = 0L, subtitles = emptySet(), subtitle = null, - autoPlay = false + autoPlay = false, + preview = false ) true } ?: run { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index be3de52b..5e4869cc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -114,10 +114,20 @@ class ResultFragmentTv : Fragment() { } } - private fun hasNoFocus(): Boolean { - val focus = activity?.currentFocus - if (focus == null || !focus.isVisible) return true - return focus == binding?.resultRoot +// private fun hasNoFocus(): Boolean { +// val focus = activity?.currentFocus +// if (focus == null || !focus.isVisible) return true +// return focus == binding?.resultRoot +// } + + /** + * Force focus any play button. + * Note that this will steal any focus if the episode loading is too slow (unlikely). + */ + private fun focusPlayButton() { + binding?.resultPlayMovie?.requestFocus() + binding?.resultPlaySeries?.requestFocus() + binding?.resultResumeSeries?.requestFocus() } private fun setRecommendations(rec: List?, validApiName: String?) { @@ -177,7 +187,7 @@ class ResultFragmentTv : Fragment() { isVisible = true } - this.animate().alpha(if (turnVisible) 1.0f else 0.0f).apply { + this.animate().alpha(if (turnVisible) 0.97f else 0.0f).apply { duration = 200 interpolator = DecelerateInterpolator() setListener(object : Animator.AnimatorListener { @@ -206,11 +216,9 @@ class ResultFragmentTv : Fragment() { episodesShadow.fade(show) episodeHolderTv.fade(show) if (episodesShadow.isRtl()) { - episodesShadow.scaleX = -1.0f - episodesShadow.scaleY = -1.0f + episodesShadowBackground.scaleX = -1f } else { - episodesShadow.scaleX = 1.0f - episodesShadow.scaleY = 1.0f + episodesShadowBackground.scaleX = 1f } } } @@ -294,9 +302,9 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(true) binding?.apply { val views = listOf( + resultDubSelection, resultSeasonSelection, resultRangeSelection, - resultDubSelection, resultEpisodes, resultPlayTrailer, ) @@ -413,7 +421,13 @@ class ResultFragmentTv : Fragment() { setHorizontal() } - resultCastItems.adapter = ActorAdaptor { + val aboveCast = listOf( + binding?.resultEpisodesShow, + binding?.resultBookmarkButton, + ).firstOrNull { + it?.isVisible == true + } + resultCastItems.adapter = ActorAdaptor(aboveCast?.id) { toggleEpisodes(false) } } @@ -454,9 +468,7 @@ class ResultFragmentTv : Fragment() { resultPlaySeries.isVisible = false resultResumeSeries.isVisible = true - if (hasNoFocus()) { - resultResumeSeries.requestFocus() - } + focusPlayButton() resultResumeSeries.text = if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull( @@ -539,9 +551,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - if (hasNoFocus()) { - resultPlayMovie.requestFocus() - } + focusPlayButton() } } } @@ -636,6 +646,9 @@ class ResultFragmentTv : Fragment() { .show() } } + + // Used to request focus the first time the episodes are loaded. + var hasLoadedEpisodesOnce = false observeNullable(viewModel.episodes) { episodes -> binding?.apply { resultEpisodes.isVisible = episodes is Resource.Success @@ -663,6 +676,10 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } + if (!hasLoadedEpisodesOnce) { + hasLoadedEpisodesOnce = true + focusPlayButton() + } } /* diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index b398b54e..6acf476a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -518,7 +518,8 @@ class ResultViewModel2 : ViewModel() { val episodeNumber = episodes[currentIndex].episode if (episodeNumber < currentMin) { currentMin = episodeNumber - } else if (episodeNumber > currentMax) { + } + if (episodeNumber > currentMax) { currentMax = episodeNumber } ++currentIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index bdf82377..ce92d723 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -11,6 +11,7 @@ import android.widget.AbsListView import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.ListView +import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible @@ -22,17 +23,22 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.AllLanguagesName +import com.lagradost.cloudstream3.AnimeSearchResponse +import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSearchBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding import com.lagradost.cloudstream3.mvvm.Resource @@ -53,8 +59,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -62,9 +68,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import java.util.concurrent.locks.ReentrantLock -const val SEARCH_PREF_TAGS = "search_pref_tags" -const val SEARCH_PREF_PROVIDERS = "search_pref_providers" - class SearchFragment : Fragment() { companion object { fun List.filterSearchResponse(): List { @@ -193,7 +196,7 @@ class SearchFragment : Fragment() { validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> if (selectedSearchTypes.toSet() != list.toSet()) { - setKey(SEARCH_PREF_TAGS, selectedSearchTypes) + DataStoreHelper.searchPreferenceTags = list selectedSearchTypes.clear() selectedSearchTypes.addAll(list) search(binding?.mainSearch?.query?.toString()) @@ -219,7 +222,7 @@ class SearchFragment : Fragment() { SearchHelper.handleSearchClickCallback(callback) } - + searchRoot.findViewById(R.id.search_src_text)?.tag = "tv_no_focus_tag" searchAutofitResults.adapter = adapter searchLoadingBar.alpha = 0f } @@ -232,13 +235,7 @@ class SearchFragment : Fragment() { //searchMagIcon.scaleX = 0.65f //searchMagIcon.scaleY = 0.65f - context?.let { ctx -> - val validAPIs = ctx.filterProviderByPreferredMedia() - selectedApis = ctx.getKey( - SEARCH_PREF_PROVIDERS, - defVal = validAPIs.map { it.name } - )!!.toMutableSet() - } + selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet() binding?.searchFilter?.setOnClickListener { searchView -> searchView?.context?.let { ctx -> @@ -286,7 +283,7 @@ class SearchFragment : Fragment() { } fun updateList(types: List) { - setKey(SEARCH_PREF_TAGS, types.map { it.name }) + DataStoreHelper.searchPreferenceTags = types arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> @@ -311,12 +308,7 @@ class SearchFragment : Fragment() { arrayAdapter.notifyDataSetChanged() } - val selectedSearchTypes = getKey>(SEARCH_PREF_TAGS) - ?.mapNotNull { listName -> - TvType.values().firstOrNull { it.name == listName } - } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + val selectedSearchTypes = DataStoreHelper.searchPreferenceTags bindChips( binding.tvtypesChipsScroll.tvtypesChips, @@ -342,7 +334,7 @@ class SearchFragment : Fragment() { } dialog.setOnDismissListener { - context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList()) + DataStoreHelper.searchPreferenceProviders = currentSelectedApis.toList() selectedApis = currentSelectedApis } updateList(selectedSearchTypes.toList()) @@ -353,10 +345,7 @@ class SearchFragment : Fragment() { val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true - selectedSearchTypes = context?.getKey>(SEARCH_PREF_TAGS) - ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList() if (isTrueTvSettings()) { binding?.searchFilter?.isFocusable = true @@ -398,7 +387,7 @@ class SearchFragment : Fragment() { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - removeKeys(SEARCH_HISTORY_KEY) + removeKeys("$currentAccount/$SEARCH_HISTORY_KEY") searchViewModel.updateHistory() } DialogInterface.BUTTON_NEGATIVE -> { @@ -510,7 +499,7 @@ class SearchFragment : Fragment() { binding?.mainSearch?.setQuery(searchItem.searchText, true) } SEARCH_HISTORY_REMOVE -> { - removeKey(SEARCH_HISTORY_KEY, searchItem.key) + removeKey("$currentAccount/$SEARCH_HISTORY_KEY", searchItem.key) searchViewModel.updateHistory() } else -> { @@ -559,4 +548,4 @@ class SearchFragment : Fragment() { .commit()*/ } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index 320687f8..839b9d3f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -14,6 +14,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -64,7 +65,7 @@ class SearchViewModel : ViewModel() { fun updateHistory() = viewModelScope.launch { ioSafe { - val items = getKeys(SEARCH_HISTORY_KEY)?.mapNotNull { + val items = getKeys("$currentAccount/$SEARCH_HISTORY_KEY")?.mapNotNull { getKey(it) }?.sortedByDescending { it.searchedAt } ?: emptyList() _currentHistory.postValue(items) @@ -87,7 +88,7 @@ class SearchViewModel : ViewModel() { if (!isQuickSearch) { val key = query.hashCode().toString() setKey( - SEARCH_HISTORY_KEY, + "$currentAccount/$SEARCH_HISTORY_KEY", key, SearchHistoryItem( searchedAt = System.currentTimeMillis(), @@ -140,4 +141,4 @@ class SearchViewModel : ViewModel() { _searchResponse.postValue(Resource.Success(list)) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index e53fa91a..4895b0d2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -205,6 +205,11 @@ class SettingsFragment : Fragment() { } } } + + // Default focus on TV + if (isTrueTv) { + settingsGeneral.requestFocus() + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index ae5f0aab..17efd276 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -80,6 +80,7 @@ val appLanguages = arrayListOf( Triple("", "日本語 (にほんご)", "ja"), Triple("", "ಕನ್ನಡ", "kn"), Triple("", "한국어", "ko"), + Triple("", "lietuvių kalba", "lt"), Triple("", "latviešu valoda", "lv"), Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 0bef5e9a..7e57fc5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -96,7 +96,7 @@ class SettingsProviders : PreferenceFragmentCompat() { this.getString(R.string.prefer_media_type_key), selectedList.map { it.toString() }.toMutableSet() ).apply() - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null //(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 62e46c08..2f796801 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -19,14 +19,16 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.network.initClient +import com.lagradost.cloudstream3.services.BackupWorkManager import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.BackupUtils.backup +import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.VideoDownloadManager @@ -48,7 +50,30 @@ class SettingsUpdates : PreferenceFragmentCompat() { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) getPref(R.string.backup_key)?.setOnPreferenceClickListener { - activity?.backup() + BackupUtils.backup(activity) + return@setOnPreferenceClickListener true + } + + getPref(R.string.automatic_backup_key)?.setOnPreferenceClickListener { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + val prefNames = resources.getStringArray(R.array.periodic_work_names) + val prefValues = resources.getIntArray(R.array.periodic_work_values) + val current = settingsManager.getInt(getString(R.string.automatic_backup_key), 0) + + activity?.showDialog( + prefNames.toList(), + prefValues.indexOf(current), + getString(R.string.backup_frequency), + true, + {}) { index -> + settingsManager.edit() + .putInt(getString(R.string.automatic_backup_key), prefValues[index]).apply() + BackupWorkManager.enqueuePeriodicWork( + context ?: AcraApplication.context, + prefValues[index].toLong() + ) + } return@setOnPreferenceClickListener true } @@ -65,7 +90,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { val builder = AlertDialog.Builder(pref.context, R.style.AlertDialogCustom) - val binding = LogcatBinding.inflate(layoutInflater,null,false ) + val binding = LogcatBinding.inflate(layoutInflater, null, false) builder.setView(binding.root) val dialog = builder.create() @@ -176,7 +201,8 @@ class SettingsUpdates : PreferenceFragmentCompat() { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context) val prefNames = resources.getStringArray(R.array.auto_download_plugin) - val prefValues = enumValues().sortedBy { x -> x.value }.map { x -> x.value } + val prefValues = + enumValues().sortedBy { x -> x.value }.map { x -> x.value } val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0) @@ -186,7 +212,8 @@ class SettingsUpdates : PreferenceFragmentCompat() { getString(R.string.automatic_plugin_download_mode_title), true, {}) { - settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() + settingsManager.edit() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 6916cafe..f9197213 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -15,8 +15,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API class SetupFragmentMedia : Fragment() { @@ -77,7 +77,7 @@ class SetupFragmentMedia : Fragment() { .apply() // Regenerate set homepage - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 96593769..e50131fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -10,6 +10,7 @@ import androidx.annotation.WorkerThread import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError @@ -90,9 +91,11 @@ object BackupUtils { ) @Suppress("UNCHECKED_CAST") - fun Context.getBackup(): BackupFile { - val allData = getSharedPrefs().all.filter { it.key.isTransferable() } - val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() } + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } + val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } val allDataSorted = BackupVars( allData.filter { it.value is Boolean } as? Map, @@ -119,46 +122,50 @@ object BackupUtils { } @WorkerThread - fun Context.restore( + fun restore( + context: Context?, backupFile: BackupFile, restoreSettings: Boolean, restoreDataStore: Boolean ) { + if (context == null) return if (restoreSettings) { - restoreMap(backupFile.settings._Bool, true) - restoreMap(backupFile.settings._Int, true) - restoreMap(backupFile.settings._String, true) - restoreMap(backupFile.settings._Float, true) - restoreMap(backupFile.settings._Long, true) - restoreMap(backupFile.settings._StringSet, true) + context.restoreMap(backupFile.settings._Bool, true) + context.restoreMap(backupFile.settings._Int, true) + context.restoreMap(backupFile.settings._String, true) + context.restoreMap(backupFile.settings._Float, true) + context.restoreMap(backupFile.settings._Long, true) + context.restoreMap(backupFile.settings._StringSet, true) } if (restoreDataStore) { - restoreMap(backupFile.datastore._Bool) - restoreMap(backupFile.datastore._Int) - restoreMap(backupFile.datastore._String) - restoreMap(backupFile.datastore._Float) - restoreMap(backupFile.datastore._Long) - restoreMap(backupFile.datastore._StringSet) + context.restoreMap(backupFile.datastore._Bool) + context.restoreMap(backupFile.datastore._Int) + context.restoreMap(backupFile.datastore._String) + context.restoreMap(backupFile.datastore._Float) + context.restoreMap(backupFile.datastore._Long) + context.restoreMap(backupFile.datastore._StringSet) } } @SuppressLint("SimpleDateFormat") - fun FragmentActivity.backup() = ioSafe { + fun backup(context: Context?) = ioSafe { + if (context == null) return@ioSafe + var fileStream: OutputStream? = null var printStream: PrintWriter? = null try { - if (!checkWrite()) { + if (!context.checkWrite()) { showToast(R.string.backup_failed, Toast.LENGTH_LONG) - requestRW() + context.getActivity()?.requestRW() return@ioSafe } val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) val ext = "txt" val displayName = "CS3_Backup_${date}" - val backupFile = getBackup() - val stream = setupStream(this@backup, displayName, null, ext, false) + val backupFile = getBackup(context) + val stream = setupStream(context, displayName, null, ext, false) fileStream = stream.openNew() printStream = PrintWriter(fileStream) @@ -198,7 +205,8 @@ object BackupUtils { val restoredValue = mapper.readValue(input) - activity.restore( + restore( + activity, restoredValue, restoreSettings = true, restoreDataStore = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 2eb2ab01..952422a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -10,7 +10,9 @@ import androidx.core.widget.doOnTextChanged import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -24,6 +26,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter +import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState @@ -31,6 +34,8 @@ import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import kotlin.reflect.KClass +import kotlin.reflect.KProperty const val VIDEO_POS_DUR = "video_pos_dur" const val VIDEO_WATCH_STATE = "video_watch_state" @@ -44,6 +49,28 @@ const val RESULT_EPISODE = "result_episode" const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" + +class UserPreferenceDelegate( + private val key: String, private val default: T //, private val klass: KClass +) { + private val klass: KClass = default::class + private val realKey get() = "${DataStoreHelper.currentAccount}/$key" + operator fun getValue(self: Any?, property: KProperty<*>) = + AcraApplication.getKeyClass(realKey, klass.java) ?: default + + operator fun setValue( + self: Any?, + property: KProperty<*>, + t: T? + ) { + if (t == null) { + removeKey(realKey) + } else { + AcraApplication.setKeyClass(realKey, t) + } + } +} + object DataStoreHelper { // be aware, don't change the index of these as Account uses the index for the art private val profileImages = arrayOf( @@ -56,6 +83,49 @@ object DataStoreHelper { R.drawable.profile_bg_teal ) + private var searchPreferenceProvidersStrings : List by UserPreferenceDelegate( + /** java moment right here, as listOf()::class.java != List(0) { "" }::class.java */ + "search_pref_providers", List(0) { "" } + ) + + private fun serializeTv(data : List) : List = data.map { it.name } + + private fun deserializeTv(data : List) : List { + return data.mapNotNull { listName -> + TvType.values().firstOrNull { it.name == listName } + } + } + + var searchPreferenceProviders : List + get() { + val ret = searchPreferenceProvidersStrings + return ret.ifEmpty { + context?.filterProviderByPreferredMedia()?.map { it.name } ?: emptyList() + } + } set(value) { + searchPreferenceProvidersStrings = value + } + + private var searchPreferenceTagsStrings : List by UserPreferenceDelegate("search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var searchPreferenceTags : List + get() = deserializeTv(searchPreferenceTagsStrings) + set(value) { + searchPreferenceTagsStrings = serializeTv(value) + } + + + private var homePreferenceStrings : List by UserPreferenceDelegate("home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var homePreference : List + get() = deserializeTv(homePreferenceStrings) + set(value) { + homePreferenceStrings = serializeTv(value) + } + + var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0)) + var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f) + var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0) + var librarySortingMode : Int by UserPreferenceDelegate("library_sorting_mode", ListSorting.AlphabeticalA.ordinal) + data class Account( @JsonProperty("keyIndex") val keyIndex: Int, @@ -77,10 +147,28 @@ object DataStoreHelper { var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0) val currentAccount: String get() = selectedKeyIndex.toString() - private fun setAccount(account: Account) { + /** + * Get or set the current account homepage. + * Setting this does not automatically reload the homepage. + */ + var currentHomePage: String? + get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") + set(value) { + val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" + if (value == null) { + removeKey(key) + } else { + setKey(key, value) + } + } + + private fun setAccount(account: Account, refreshHomePage: Boolean) { selectedKeyIndex = account.keyIndex showToast(account.name) MainActivity.bookmarksUpdatedEvent(true) + if (refreshHomePage) { + MainActivity.reloadHomeEvent(true) + } } private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) { @@ -112,8 +200,7 @@ object DataStoreHelper { accounts = currentAccounts.toTypedArray() // update UI - setAccount(getDefaultAccount(context)) - MainActivity.bookmarksUpdatedEvent(true) + setAccount(getDefaultAccount(context), true) dialog?.dismissSafe() } @@ -161,8 +248,13 @@ object DataStoreHelper { currentAccounts.add(currentEditAccount) } + // Save the current homepage for new accounts + val currentHomePage = DataStoreHelper.currentHomePage + // set the new default account as well as add the key for the new account - setAccount(currentEditAccount) + setAccount(currentEditAccount, false) + DataStoreHelper.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() dialog.dismissSafe() @@ -204,7 +296,7 @@ object DataStoreHelper { ) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account) + setAccount(account, true) builder.dismissSafe() }, addAccountCallback = { @@ -353,7 +445,7 @@ object DataStoreHelper { removeKeys(folder2) } - fun deleteBookmarkedData(id : Int?) { + fun deleteBookmarkedData(id: Int?) { if (id == null) return removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 5edff7a1..d89e67fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -82,6 +82,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz import com.lagradost.cloudstream3.extractors.MixDropCh @@ -118,9 +119,6 @@ import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape import com.lagradost.cloudstream3.extractors.Solidfiles -import com.lagradost.cloudstream3.extractors.SpeedoStream -import com.lagradost.cloudstream3.extractors.SpeedoStream1 -import com.lagradost.cloudstream3.extractors.SpeedoStream2 import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u import com.lagradost.cloudstream3.extractors.StreamSB @@ -380,6 +378,15 @@ open class ExtractorLink constructor( val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 val isDash : Boolean get() = type == ExtractorLinkType.DASH + fun getAllHeaders() : Map { + if (referer.isBlank()) { + return headers + } else if (headers.keys.none { it.equals("referer", ignoreCase = true) }) { + return headers + mapOf("referer" to referer) + } + return headers + } + constructor( source: String, name: String, @@ -748,9 +755,7 @@ val extractorApis: MutableList = arrayListOf( Vido(), Linkbox(), Acefile(), - SpeedoStream(), - SpeedoStream1(), - SpeedoStream2(), + Minoplres(), // formerly SpeedoStream Zorofile(), Embedgram(), Mvidoo(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index b2c4aa5c..0dce0b2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -109,18 +109,19 @@ class InAppUpdater { releases.sortedWith(compareBy { versionRegex.find(it.name)?.groupValues?.get(2) }).toList().lastOrNull()*/ - val found = + val foundList = response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.filter { it.content_type == "application/vnd.android.package-archive" } - .getOrNull(0)?.name?.let { it1 -> + release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 - )?.groupValues?.get(2) + )?.groupValues?.let { + it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() + } } - }).toList().lastOrNull() - + }).toList() + val found = foundList.lastOrNull() val foundAsset = found?.assets?.getOrNull(0) val currentVersion = packageName?.let { packageManager.getPackageInfo( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 11dfa441..298f1601 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -71,7 +71,7 @@ object M3u8Helper2 { private val QUALITY_REGEX = Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") private val TS_EXTENSION_REGEX = - Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways + Regex("""#EXTINF:(([0-9]*[.])?[0-9]+|).*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts private fun absoluteExtensionDetermination(url: String): String? { @@ -115,13 +115,22 @@ object M3u8Helper2 { private fun selectBest(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { - if (it.quality != null && it.quality <= 1080) it.quality else 0 - }.filter { + it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0 + }/*.filter { listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) - } + }*/ return result.lastOrNull() } + private fun selectWorst(qualities: List): M3u8Helper.M3u8Stream? { + val result = qualities.sortedBy { + it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0 + }/*.filter { + listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) + }*/ + return result.firstOrNull() + } + private fun getParentLink(uri: String): String { val split = uri.split("/").toMutableList() split.removeLast() @@ -173,14 +182,20 @@ object M3u8Helper2 { return list } + data class TsLink( + val url : String, + val time : Double?, + ) + data class LazyHlsDownloadData( private val encryptionData: ByteArray, private val encryptionIv: ByteArray, - private val isEncrypted: Boolean, - private val allTsLinks: List, - private val relativeUrl: String, - private val headers: Map, + val isEncrypted: Boolean, + val allTsLinks: List, + val relativeUrl: String, + val headers: Map, ) { + val size get() = allTsLinks.size suspend fun resolveLinkWhileSafe( @@ -228,9 +243,9 @@ object M3u8Helper2 { @Throws suspend fun resolveLink(index: Int): ByteArray { if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts") - val url = allTsLinks[index] + val ts = allTsLinks[index] - val tsResponse = app.get(url, headers = headers, verify = false) + val tsResponse = app.get(ts.url, headers = headers, verify = false) val tsData = tsResponse.body.bytes() if (tsData.isEmpty()) throw ErrorLoadingException("no data") @@ -244,15 +259,16 @@ object M3u8Helper2 { @Throws suspend fun hslLazy( - qualities: List + qualities: List, selectBest : Boolean = true ): LazyHlsDownloadData { if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty") - val selected = selectBest(qualities) ?: qualities.first() + val selected = if(selectBest) { selectBest(qualities) } else { selectWorst(qualities) } ?: qualities.first() val headers = selected.headers val streams = qualities.map { m3u8Generation(it, false) }.flatten() // this selects the best quality of the qualities offered, // due to the recursive nature of m3u8, we only go 2 depth - val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) + val innerStreams = streams.ifEmpty { listOf(selected) } + val secondSelection = if(selectBest) { selectBest(innerStreams) } else { selectWorst(innerStreams) } ?: throw IllegalArgumentException("qualities has no streams") val m3u8Response = @@ -285,12 +301,14 @@ object M3u8Helper2 { } val relativeUrl = getParentLink(secondSelection.streamUrl) val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts -> - val value = ts.groupValues[1] - if (isNotCompleteUrl(value)) { + val time = ts.groupValues[1] + val value = ts.groupValues[3] + val url = if (isNotCompleteUrl(value)) { "$relativeUrl/${value}" } else { value } + TsLink(url = url, time = time.toDoubleOrNull()) }.toList() if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt index dcb1e047..7ff7b067 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt @@ -1,12 +1,12 @@ package com.lagradost.cloudstream3.utils -import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build import android.os.IBinder import android.util.Log @@ -54,7 +54,11 @@ class PackageInstallerService : Service() { UPDATE_CHANNEL_NAME, UPDATE_CHANNEL_DESCRIPTION ) - startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build(), FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else{ + startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build()) + } } private val updateLock = Mutex() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 038a2f11..9b40e70e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -71,7 +71,7 @@ object UIHelper { val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) - fun Activity.checkWrite(): Boolean { + fun Context.checkWrite(): Boolean { return (ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE diff --git a/app/src/main/res/drawable/episodes_shadow.xml b/app/src/main/res/drawable/episodes_shadow.xml index b4cdd382..a77cbf25 100644 --- a/app/src/main/res/drawable/episodes_shadow.xml +++ b/app/src/main/res/drawable/episodes_shadow.xml @@ -1,6 +1,8 @@ + android:centerColor="?attr/primaryBlackBackground" + android:centerX="0.2" + android:endColor="?attr/primaryBlackBackground" + android:startColor="@color/transparent" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/outline.xml b/app/src/main/res/drawable/outline.xml index 30077a98..7b436c7d 100644 --- a/app/src/main/res/drawable/outline.xml +++ b/app/src/main/res/drawable/outline.xml @@ -2,11 +2,9 @@ - - \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable.xml b/app/src/main/res/drawable/outline_drawable.xml index 8eec2d0b..16eba83c 100644 --- a/app/src/main/res/drawable/outline_drawable.xml +++ b/app/src/main/res/drawable/outline_drawable.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable_less.xml b/app/src/main/res/drawable/outline_drawable_less.xml index db74a092..aa3a8d0d 100644 --- a/app/src/main/res/drawable/outline_drawable_less.xml +++ b/app/src/main/res/drawable/outline_drawable_less.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/video_frame.xml b/app/src/main/res/drawable/video_frame.xml new file mode 100644 index 00000000..19fcf26d --- /dev/null +++ b/app/src/main/res/drawable/video_frame.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index 54df59a8..368fa770 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -17,6 +17,7 @@ android:layout_width="100dp" android:layout_height="wrap_content" android:orientation="vertical" + android:focusable="true" android:padding="5dp"> diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index ac660ccd..36cb5f42 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -114,6 +114,22 @@ android:nextFocusRight="@id/home_switch_account" /> + + + + + android:nextFocusRight="@id/home_preview_search_button" + android:nextFocusDown="@id/home_preview_play_btt" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 87de7186..9d748c5a 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -875,7 +875,7 @@ android:descendantFocusability="afterDescendants" android:paddingBottom="100dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/result_episode_both_tv" /> + tools:listitem="@layout/result_episode" /> diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 4d236d78..feaf6fbc 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -535,129 +535,151 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - - - - - - - - - - - - + tools:visibility="visible"> - + - style="@style/Widget.AppCompat.ProgressBar" - android:layout_gravity="center" - android:layout_width="50dp" - android:layout_height="50dp" />--> + + - - - - + android:focusableInTouchMode="false" + android:importantForAccessibility="no" + android:src="@drawable/episodes_shadow" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/shadow_space_2" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + - @@ -99,7 +100,7 @@ android:id="@+id/search_autofit_results" android:layout_width="match_parent" android:layout_height="match_parent" - + android:layout_marginStart="@dimen/navbar_width" android:background="?attr/primaryBlackBackground" android:clipToPadding="false" android:descendantFocusability="afterDescendants" @@ -141,6 +142,7 @@ android:nextFocusLeft="@id/nav_rail_view" android:nextFocusUp="@id/tvtypes_chips" android:nextFocusDown="@id/search_clear_call_history" + android:tag = "@string/tv_no_focus_tag" android:paddingBottom="50dp" android:visibility="visible" tools:listitem="@layout/search_history_item" /> diff --git a/app/src/main/res/layout/library_viewpager_page.xml b/app/src/main/res/layout/library_viewpager_page.xml index 7d278cff..aa9745fb 100644 --- a/app/src/main/res/layout/library_viewpager_page.xml +++ b/app/src/main/res/layout/library_viewpager_page.xml @@ -5,5 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" + android:focusable="false" + android:tag="tv_no_focus_tag" tools:listitem="@layout/home_result_grid_expanded" /> diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 5592f3a6..38df4c5b 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -86,7 +86,6 @@ + + android:importantForAccessibility="no" + android:visibility="gone" /> + + + - - - + + + + + - - - + - - + tools:ignore="ContentDescription" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - - + + + + + - + - diff --git a/app/src/main/res/layout/player_quality_profile_item.xml b/app/src/main/res/layout/player_quality_profile_item.xml index 0eab2407..5178a12f 100644 --- a/app/src/main/res/layout/player_quality_profile_item.xml +++ b/app/src/main/res/layout/player_quality_profile_item.xml @@ -10,6 +10,7 @@ android:id="@+id/card_view" android:layout_width="0dp" android:layout_height="0dp" + android:focusable="true" app:layout_constraintDimensionRatio="1" android:layout_marginStart="10dp" android:animateLayoutChanges="true" diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index 7351a41f..6bf8006b 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -27,6 +27,7 @@ android:layout_height="wrap_content" android:layout_rowWeight="1" android:layout_marginTop="10dp" + android:focusable="true" android:foreground="@drawable/outline_drawable_forced" android:gravity="center_vertical" android:orientation="horizontal"> @@ -66,8 +67,9 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusUp="@id/profiles_click_settings" + android:nextFocusRight="@id/sort_subtitles" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" tools:layout_height="100dp" tools:listitem="@layout/sort_bottom_single_choice" /> @@ -94,6 +96,7 @@ android:layout_height="wrap_content" android:layout_rowWeight="1" android:layout_marginTop="10dp" + android:focusable="true" android:foreground="@drawable/outline_drawable_forced" android:orientation="horizontal" android:paddingTop="10dp" @@ -139,8 +142,10 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" + android:nextFocusUp="@id/subtitles_click_settings" android:nextFocusLeft="@id/sort_providers" - android:nextFocusRight="@id/cancel_btt" + android:nextFocusRight="@id/apply_btt" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" tools:layout_height="200dp" tools:listfooter="@layout/sort_bottom_footer_add_choice" diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 86a8a756..2af3c339 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -42,8 +42,8 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_less" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/sort_subtitles" + android:nextFocusDown="@id/profile_text_editable" android:requiresFadingEdge="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layout_height="100dp" @@ -92,6 +92,8 @@ android:layout_height="50dp" android:background="?attr/selectableItemBackgroundBorderless" android:padding="12dp" + android:focusable="true" + android:nextFocusLeft="@id/sort_sources" android:src="@drawable/baseline_help_outline_24" android:contentDescription="@string/help" /> @@ -115,8 +117,10 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_less" - android:nextFocusLeft="@id/sort_providers" - android:nextFocusRight="@id/cancel_btt" + android:nextFocusLeft="@id/sort_sources" + android:nextFocusRight="@id/apply_btt" + android:nextFocusUp="@id/help_btt" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layout_height="200dp" diff --git a/app/src/main/res/layout/result_episode.xml b/app/src/main/res/layout/result_episode.xml index 80ff4bec..b56cdb1d 100644 --- a/app/src/main/res/layout/result_episode.xml +++ b/app/src/main/res/layout/result_episode.xml @@ -11,7 +11,9 @@ android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" app:cardCornerRadius="@dimen/rounded_image_radius" - app:cardElevation="0dp"> + app:cardElevation="0dp" + android:foreground="@drawable/outline_drawable" + > diff --git a/app/src/main/res/layout/result_episode_both.xml b/app/src/main/res/layout/result_episode_both.xml deleted file mode 100644 index 61102e84..00000000 --- a/app/src/main/res/layout/result_episode_both.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_old.xml b/app/src/main/res/layout/result_episode_both_old.xml new file mode 100644 index 00000000..6472ecc1 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_old.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_tv.xml b/app/src/main/res/layout/result_episode_both_tv.xml deleted file mode 100644 index 13888b7e..00000000 --- a/app/src/main/res/layout/result_episode_both_tv.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_tv_old.xml b/app/src/main/res/layout/result_episode_both_tv_old.xml new file mode 100644 index 00000000..f273a118 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_tv_old.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 75292965..76e8c434 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -8,6 +8,7 @@ android:layout_height="wrap_content" android:layout_marginBottom="10dp" + android:foreground="@drawable/outline_drawable" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" diff --git a/app/src/main/res/layout/result_episode_large_tv.xml b/app/src/main/res/layout/result_episode_large_tv.xml deleted file mode 100644 index 5a9dee30..00000000 --- a/app/src/main/res/layout/result_episode_large_tv.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large_tv_old.xml b/app/src/main/res/layout/result_episode_large_tv_old.xml new file mode 100644 index 00000000..3a7cef3c --- /dev/null +++ b/app/src/main/res/layout/result_episode_large_tv_old.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv.xml b/app/src/main/res/layout/result_episode_tv.xml deleted file mode 100644 index 53590b6b..00000000 --- a/app/src/main/res/layout/result_episode_tv.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv_old.xml b/app/src/main/res/layout/result_episode_tv_old.xml new file mode 100644 index 00000000..62546cf9 --- /dev/null +++ b/app/src/main/res/layout/result_episode_tv_old.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_history_item.xml b/app/src/main/res/layout/search_history_item.xml index 3e9ee833..4c50d0c0 100644 --- a/app/src/main/res/layout/search_history_item.xml +++ b/app/src/main/res/layout/search_history_item.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/outline_drawable_less" - + android:focusable="true" android:nextFocusRight="@id/home_history_remove" android:orientation="horizontal"> - + + + + + - - + + - diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 42eba3cc..47c69a28 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -1,2 +1,8 @@ - + + السرعة (%.2fx) + غير المصدر + حتنزل الحلقة %d ب + التنزيلات + %s الحلقة %d + diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 5a799eb4..32f4dcd7 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -2,4 +2,112 @@ %s ክፍል %d ተዋናዮች: %s + ፍጥነት(%.2fx) + ቀጣይ በዘፈቀደ + ዳራ ቅድም እይታ + %dሰዓት %dደቂቃ + ፖስተር + የወረዱ + አዲስ ማሻሻያ ተገኝቷል! +\n%s -> %s + ተመለስ + ተጨማሪ አማራጮች + በማየት ላይ + ዘውጎች + የክፍሉ ፖስተር + %sን ፈልግ… + መሙያ + አቅራቢ ቀይር + ፍለጋ… + ተመዘነ: %.1f + አሳሽ + %dቀን %dሰዓት %dደቂቃ + ቀጣይ ክፍል + %dደቂቃ + %d ደቂቃ + ፖስተር + በአሳሽ ውስጥ ይክፈቱ + ውሂብ የለም + መነሻ + መጫንን ዝለል + ክፍል %d በ ይለቀቃል + ማጋሪያ + ዋና ፖስተር + ቅንብሮች + መፈለጊያ + በመጫን ላይ… + CloudStream + በCloudStream አጫውት + ያስወግዱ + ዕልባቶች + ማውረድ ተጀምሯል + ሙቪ አጫውት + አጽዳ + የዳርቻ ቀለም + ዕልባቶችን ማጣሪያ + በእቅድ ላይ + ማዘመን ተጀምሯል + ቅዳ + ተጨማሪ መረጃ + አገናኞችን መጫን ላይ ስህተት + የጠርዝ ዓይነት + ማውረድ ተከናውኗል + በማውረድ ላይ + ክፍልን አጫውት + የአጫዋች ፍጥነት + የተተወ + የዳራ ቀለም + ማውረድ ለአፍታ አቁም + ትርጉም ድምጽ + የትርጉም ጽሑፍ ቅንብሮች + የዊንዶው ቀለም + Torrent አጫውት + ማውረድ ተቋርጧል + ደብቅ + አጽድቅ + የትርጉም ጽሑፎች + ማውረድ ቆሟል + ማውረጃ + ግንኙነትን እንደገና ይሞሩ… + ፋይል አጥፋ + ወርዷል + ማውረድ ቀጥል + የጽሑፍ ቀለም + የተጠናቀቀ + ምንም + የፊልም ማስታወቂያ አጫውት + የቀጥታ ስርጭት አጫውት + ፋይል አጫውት + እንደገና በማየት ላይ + ሰርዝ + ወደ ኋላ መመለሻ + መረጃ + ያስቀምጡ + ማውረድ አልተሳካም + ምንጮች + ትርጉም ጽሁፍ + ዥረት + በመቆየት ላይ + አጫውት + ውስጣዊ ማከማቻ + ዝጋ + የምልከታ ሁኔታን ያቀናብሩ + ወደ ነባሪ ዳግም ለማስጀመር ጫን አድርገው ይያዙ + የቅርጸ-ቁምፊ መጠን + ቋንቋን በራስ ይምረጥ + መመልከትዎን ይቀጥሉ + ቋንቋዎችን ያውርዱ + አቅራቢዎችን በመጠቀም ይፈልጉ + %d ሙዝ ለዴቭሎፐሮቹ ተሰጥቷል + ይህ አቅራቢ በትክክል እንዲሰራ ቪፒኤን ሊያስፈልግ ይችላል + ምንም ሙዝ አልተሰጠም + የትርጉም ጽሑፍ ቋንቋ + ሜታዳታ በድረ-ገጹ ላይ አይገኝም፣ ቪድዮ መጫን ሜታዳታ ድረ-ገጹ ላይ ከሌለ አይሳካም። + ይህ አቅራቢ ቶረንት ነው፣ ቪፒኤን ይመከራል + ቅርጸ-ቁምፊ + መግለጫ + ያስወግዱ + ተጨማሪ መረጃ + ዓይነቶችን በመጠቀም ይፈልጉ + ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 0c11f7e9..8805ec5d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -99,13 +99,13 @@ إضغط بإستمرار لإعادة التعيين للإعدادات الافتراضية إستيراد خطوط بوضعها هنا %s متابعة المشاهدة - حذف - مزيد من المعلومات + ازالة + المزيد من المعلومات قد تكون هناك حاجة إلى VPN لكي يعمل هذا المزود بشكل صحيح - هذا المزود هو تورنت ، يوصى باستخدام شبكة ظاهرية خاصة + هذا المزود هو تورنت، يوصى باستخدام شبكة ظاهرية خاصة لا يتم توفير البيانات الوصفية بواسطة الموقع، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع. الوصف - لم يتم العثور على وصف + لم يتم العثور على حبكة لم يتم العثور على وصف عرض سجل الاخطاء 🐈 نافذة منبثقة @@ -585,4 +585,5 @@ لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) لقد صوتت بالفعل + معدل النسخ الإحتياطي diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index d76aa94b..e9ae65a1 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -159,7 +159,7 @@ Скрий избраното видео качество в резултатите от търсенето Автоматични актуализации на плъгини Показвай актуализации на приложението - Автоматично търси нови актуализации при стартиране + Автоматично търси нови актуализации при стартиране на приложението. Актуализация до експериментални версии Търсете експериментални актуализации вместо само пълни версии Github @@ -223,8 +223,8 @@ Филм Серия Анимационен филм - @string/anime - @string/ova + Аниме + ОВА Торент Документален филм Азиатска драма @@ -260,7 +260,7 @@ Не показвай отново Пропуснете тази актуализация Актуализация - Предпочитано качество за гледане + Предпочитано качество за гледане (през WiFi) Максимален брой знаци за заглавие във видеоплейъра Разделителна способност на видео плейъра Размер на видео буфера @@ -505,4 +505,45 @@ Библиотека Предпочитано качество за гледане (Мобилни данни) Начало + Избери режим, да филтрира изтегляне на добавки + raw.githubusercontent.com Прокси + Неуспешно свързване с GitHub. Включване на jsDelivr прокси… + Заобикаля блокирането на GitHub с помощта на jsDelivr. Може да доведе до забавяне на актуализациите с няколко дни. + Тест на доставчик + Деактивиране + Android TV + Заобикаляне на интернет доставчик + Няма намерени добавки в този архив. + Архивът не е намерен, проверете уеб-адреса и пробвайте с VPN + Оценка (От висока до ниска) + Сортирай по + Стоп + Рестартиране + Сортиране + Оценка (от ниска до висока) + Абонирани сте за %s + Обновяване на абонирани сериали + Мобилни данни + Абонаменти + Връщане + Избиране на библиотека + Отписахте се от %s + Профили + По азбучен ред (Z до A) + Създаването на потребителският интерфейс е неуспешно, това е ГОЛЯМ БЪГ и трябва да бъде докладван незабавно %s + Редактиране + Wi-Fi + Помощ + Списъкът е празен. Опитайте да превключите на друг. + Профил %d + По азбучен ред (A до Z) + Отваряне с + Вашата библиотека е празна :( +\nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека. + Използване + Епизод %d е публикуван! + Намерен е файл за безопасен режим! +\nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат. + Вече сте гласували + Задаване по подразбиране diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index ba754fa5..c55c8943 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -127,9 +127,9 @@ বিরতি দিতে মাঝে চাপুন সিস্টেম এর উজ্জ্বলতা ব্যবহার করুন ট্রেইলার চালু করুন - ভিডিওপ্লেয়ার এ সময় নিয়ন্ত্রণ করতে, ডানে অথবা বামে সোয়াইপ করুন + ভিডিওর সময় নিয়ন্ত্রণ করতে, ডানে অথবা বামে সোয়াইপ করুন সেটিংস পরিবর্তন করতে সোয়াইপ করুন - উজ্জ্বলতা অথবা স্বরমাত্রা পরিবর্তন করতে যথাক্রমে বামে অথবা ডানে সোয়াইপ করুন + উজ্জ্বলতা বা ভলিউম পরিবর্তন করতে বাম বা ডান দিকে উপরে বা নিচে স্লাইড করুন স্বয়ংক্রিয়ভাবে পরবর্তী পর্ব চালান বর্তমান পর্বটি শেষ হলে পরের পর্বটি চালান থামতে দুইবার চাপুন @@ -143,10 +143,11 @@ হালনাগাদ ও ব্যাকআপ অ্যাপ এর হালনাগাদ দেখান খুঁজতে সোয়াইপ করুন - @string/result_poster_img_des + পোস্টার @string/home_play আগাতে ডবল ট্যাপ করুন আইজেনগ্রাভি মোড আপডেট শুরু হয়েছে ব্রাউজার + লগ diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 016fbe43..7116a167 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -127,14 +127,14 @@ Modo Eigengravy Adiciona um botão de velocidade no player Deslize para avançar o vídeo - Deslize para a esq. ou dir. para controlar o tempo no player + Deslize de lado à lado para controlar a posição no vídeo Deslize para mudar as configurações - Deslize para o lado esq. ou dir. para ajustar brilho ou volume + Deslize para cima ou para baixo, para ajustar brilho ou volume Toque duplo para avançar o vídeo Toque duplo para pausar Segundos avançados no player Toque duplo no lado direito ou esquerdo para regredir ou avançar vídeo - Toque no meio para pausar + Toque duas vezes no meio para pausar Usar brilho do sistema Usar brilho do sistema no player ao invés da sobreposição escura Atualizar progresso assistido @@ -143,8 +143,8 @@ Fazer Backup Arquivo de Backup carregado Falha em restaurar dados do arquivo %s - Dados salvos com sucesso - Permissões de armazenamento faltando, por favor tente de novo + Dados salvos + Permissões de armazenamento faltando. Por favor tente novamente. Erro no backup de %s Procurar Contas @@ -160,7 +160,7 @@ Esconder qualidades de vídeo selecionadas nos resultados da pesquisa Atualizações de plugin automáticas Mostrar atualizações do app - Automaticamente procurar por novas atualizações ao abrir + Automaticamente procurar por novas atualizações ao abrir. Atualizar para pré-lançamento Procura atualizações do pré-lançamento ao invés de apenas do lançamento oficial Github @@ -223,7 +223,7 @@ Série Desenho Animado Anime - @string/ova + OVA Torrent Documentário Drama Asiático @@ -264,7 +264,7 @@ Comprimento do buffer do vídeo Cache do vídeo em disco Limpar cache de vídeo e imagem - Causará travamentos aleatórios se definido muito alto. Não mude caso tiver pouca memória RAM, como um Android TV ou um telefone antigo + Causará travamentos se o valor escolhido for muito alto em dispositivos com pouca memória RAM, como um Android TV. Causa problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV. DNS sobre HTTPS Útil para burlar bloqueios de provedores de internet @@ -326,7 +326,7 @@ /\?\? /%d %s autenticado - Falha em autenticar para %s + Não foi possível fazer login em %s Nenhum Normal @@ -340,8 +340,8 @@ Sincronizar legendas 1000 ms Atraso de legenda - Use isto se as legendas forem mostradas %dms adiantadas - Use isto se as legendas forem mostradas %dms atrasadas + Use isto se as legendas forem mostradas %d ms adiantadas + Use isto se as legendas forem mostradas %d ms atrasadas Sem atraso de legenda - रफ्तार (%.2fx) + स्पीड (%.2fx) नया अपडेट आया है! \n%s -> %s होम खोजें - डाउनलोड - सेटिंग - डेटा उपलब्ध नही है - अन्य ऑप्शन + डाउनलोडस + सेटिंग्स + डेटा उपलब्ध नहीं + और विकल्प अगला एपिसोड शैलियां - शेयर + साझा करें ब्राउज़र में खोलें देखा जा रहा है होल्ड पर पूरा देखा अधूरा छोड़ा देखने की योजना - मूवी चलाये - टोरेंट चलाये - सूत्र - फिरसे प्रयास करे… + मूवी चलाएं + टोरेंट चलाएं + सोर्स + पुनः प्रयास करें… वापिस जाए - एपिसोड चलाये + एपिसोड चलाएं डाउनलोड डाउनलोड किया गया डाउनलोड हो रहा डाउनलोड रोका गया - डाउनलोड शुरू - डाउनलोड नही हो पाया + डाउनलोड शुरू हुआ + डाउनलोड विफल डाउनलोड रद्द - डाउनलोड पूरा हुआ - लिंक में गड़बड़ है + डाउनलोड मुकम्मल + लिंक लोड करने में त्रुटि अंदरूनी स्टोरेज - फ़ाइल डिलीट - फ़ाइल चलाये - डाउनलोड फिर शुरू करे - डाउनलोड रोके - स्वचालित त्रुटि रिपोर्ट रोकें + फ़ाइल मिटाएँ + फ़ाइल चलाएं + डाउनलोड फिर शुरू करें + डाउनलोड रोकें + स्वचालित बग रिपोर्टिंग अक्षम करें और जानकारी - छिपाये - चलाये + छिपाएं + चलाएं जानकारी बुकमार्क छांटे बुकमार्क्स हटाएँ - लागू करे - रद्द करे - प्लेयर की रफ्तार - सूत्रों से छांटे - प्रकार से छांटे - %d केले दिए गए - कोई केले नही दिए गए - रिसेट करने ले किये दबाये रखे + लागू करें + रद्द करें + प्लेयर स्पीड + प्रोवाइडरों का उपयोग कर खोजें + प्रकार का उपयोग करके खोजें + %d केले डेवलपर्स को दिए गए + कोई केले नहीं दिए गए + डिफ़ॉल्ट पर रीसेट करने के लिए दबाए रखें देखना जारी रखें हटाएँ अधिक जानकारी - इस सूत्र को इस्तेमाल करने के लिए एक VPN की ज़रूरत पड़ सकती है - यह सूत्र एक torrent है, एक VPN इस्तेमाल करने की सिफारिश की जाती है + इस प्रोवाइडर को सही ढंग से काम करने के लिए VPN की ज़रूरत पड़ सकती है + यह प्रोवाइडर एक torrent है, एक VPN इस्तेमाल करने की सिफारिश की जाती है वर्णन - कोई विषय नही मिला - कोई वर्णन नही मिला - आपकी वीडियो एक छोटे से डब्बे में चलाता है - काले बॉर्डर को हटाता है - प्लेयर की subtitle सेटिंग्स - प्लेयर में वीडियो की रफ्तार धिमी या तेज़ करता है - दाएं या बाएं तरफ स्वाइप करने से वीडियो को आगे पीछे करता है - दाएं तरफ या बाएं तरफ स्वाइप करने से रोशिनी और आवाज़ को ऊपर नीचे करता है - दो बार दाएं या बाएं तरफ दबाने से वीडियो को आगे या पीछे करा जा सकता है + कोई विषय नहीं मिला + कोई वर्णन नहीं मिला + अन्य ऐप्स के ऊपर एक लघु प्लेयर में प्लेबैक जारी रखता है + काले बॉर्डर हटाएँ + प्लेयर उपशीर्षक सेटिंग्स + प्लेयर में स्पीड विकल्प जोड़ता है + किसी वीडियो में अपनी स्थिति नियंत्रित करने के लिए एक ओर से दूसरी ओर स्वाइप करें + चमक या वॉल्यूम बदलने के लिए बाईं या दाईं ओर ऊपर या नीचे स्लाइड करें + आगे या पीछे करने के लिए दाईं या बाईं ओर दो बार टैप करें खोजें जानकारी - नतीजों को सूत्रों के हिसाब से बांटकर दिखता है - सिर्फ दुर्घटना होने पे आपकी कुछ जानकारी भेजी जाएगी - आपकी जानकारी नही भेजी जाएगी - हर बार खुलने पे नए अपडेट के लिए जांच करेगा - पूरी रिलीस के बजाए पूर्व रिलीस की जांच करेगा - हमारा एक Light novel app - हमारा एक Anime app + खोज नतीजों को प्रोवाइडरों के हिसाब से अलग-अलग आपको दिखाता है + केवल क्रैश पर जानकारी भेजी जाएगी + आपकी जानकारी नहीं भेजी जाएगी + हर बार एप खुलने पर स्वचालित रूप से नए अपडेट खोजें। + केवल पूर्ण रिलीज़ के बजाय प्रीरिलीज़ अपडेट खोजें + उन्हीं डेवलपर्स द्वारा Light novel ऐप + उन्हीं डेवलपर्स द्वारा Anime ऐप Discord से जुड़िये - इस प्रोग्राम के निर्माता को केला चढ़ाये + इस प्रोग्राम के निर्माता को केला दें केले दे दिए गए एप्प की भाषा - यह सूत्र क्रोमकास्ट का समर्थन नही करता - कोई लिंक नही मिले - लिंक को क्लिपबोर्ड पे कॉपी करदिया गया - चलाये - असुविधा के लिए खेद है, यह एप्प क्रैश हो गया है । एक गुमनाम रिपोर्ट निर्माताओं को भेज दी गयी है । + यह प्रोवाइडर क्रोमकास्ट का समर्थन नहीं करता + कोई लिंक नहीं मिले + लिंक क्लिपबोर्ड पर कॉपी किया गया + एपिसोड चलायें + क्षमा करें, एप्प क्रैश हो गया है । निर्माताओं को एक अनाम बग रिपोर्ट भेजी जाएगी फ़ाइल डिलीट करें डिलीट - रोके - वापिस चलाये - एज इस चीज़ को हमेशा के लिए नष्ट कर देगा %s + रोकें + फिर से चलाएं + इससे %s स्थायी रूप से हट जाएगा \nक्या आपका निर्णय निश्चित है \? अभी चालू है - खत्म हो गया है - स्तिथि + मुकम्मल हुया + स्थिति साल रेटिंग अवधि - सूत्र - Synopsis - खाली + साइट + सारांश + ख़ाली इस्तेमाल में एप्प - मूवी + मूवीज टीवी सीरियल कार्टून - अनिमे + एनिमे टोरेंट क्रोमकास्ट एपिसोड - कक्रोमकास्ट मिरर - एप्प मैं चलाये - %s में चलाए - Browser में चलाए + क्रोमकास्ट मिरर + एप्प में चलाएं + %s में चलाएं + Browser में चलाएं लिंक कॉपी करें - डाउनलोड करे + डाउनलोड करें मिरर डाउनलोड - लिंक फिरसे लोड करें - कोई अपडेट नही मिला - अपडेट के लिए खोजे + लिंक दोबारा लोड करें + कोई अपडेट नहीं मिला + अपडेट के लिये जांचें ताला - आकार - सूत्र + आकार बदलें + सोर्स OP स्किप करें - फिरसे ना दिखाए + फिर ना दिखाएँ अपडेट - पसंदीदा देखने की क्वालिटी + देखने की तरजीही क्वालिटी (वाईफ़ाई) ISP ब्लॉक से छुटकारा पाएं - सूत्र की भाषाएं + प्रोवाइडर की भाषाएं ऐप का लेआउट - पसंदीदा मीडिया - डाउनलोड करने का मार्ग - Dubbed या Subbed Anime दिखाये + तरजीही मीडिया + डाउनलोड पथ + डब किए /सबब् किए एनीमे दिखाएं टीवी लेआउट फ़ोन लेआउट मुख्य रंग - ऐप का रंग + ऐप का थीम देखा हुआ चिह्नित करें इतिहास - भाग %d जारी होगा + भाग %d रिलीज़ किया जाएगा %dd %dh %dm %dh %dm %dm - विज्ञापन अगला रैंडम - वापस जाओ + वापिस जाओ पोस्टर - पृष्ठभूमि का पूर्वावलोकन करें - प्रदाता बदलें + प्रीव्यू बैकग्राउंड + प्रोवाइडर बदलें Cast: %s मुख्य पोस्टर एपिसोड का पोस्टर %s Ep %d + पूरक + रेटिंग: %.1f + %s पर खोजें… + खोजें… + %d मिनट + क्लाउडस्ट्रीम + क्लाउडस्ट्रीम के साथ चलाएं diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 477ab92b..84915a38 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -28,7 +28,7 @@ Preview Background Brzina (%.2fx) - Ocijenjeno: %.1f + Ocjena: %.1f Pronađeno novo ažuriranje! \n%s -> %s Umetak @@ -89,7 +89,7 @@ Informacije Filtriraj oznake Oznake - Makni + Ukloni Postavi status gledanja Primijeni Poništi @@ -117,7 +117,7 @@ Držite za vraćanje na zadane postavke Uvezi fontove tako da ih postavite u %s Nastavite s gledanjem - Makni + Ukloni Više informacija @string/home_play Za ispravan rad ovog pružatelja usluga može biti potreban VPN @@ -130,7 +130,7 @@ Picture-in-picture Nastavlja reprodukciju u minijaturnom playeru povrh drugih aplikacija Gumb za promjenu veličine playera - Ukloni crne rubove + Uklaja crne rubove Titlovi Postavke titlova playera Chromecast Titlovi @@ -151,7 +151,7 @@ Koristi svijetlinu u sustavu Koristi svjetlinu sustava u playeru aplikacija umjesto tamnog preklopa Ažuriraj napredak gledanja - Automatski sinkroniziraj svoj trenutni napredak u epizodi + Automatski sinkronizira vaš trenutni napredak u filmu ili epizodi Vraćanje podataka iz sigurnosne kopije Sigurnosno kopiranje podataka Učitana datoteka sigurnosne kopije @@ -306,7 +306,7 @@ Omogući NSFW na podržanim pružateljima usluga Kodiranje titlova Pružatelji usluga - Respored + Raspored Auto TV izgled Izgled za telefone @@ -314,7 +314,7 @@ Primarna boja Tema aplikacije Mjesto naslova postera - Stavi naslov ispod postera + Stavlja naslov ispod postera lozinka123 MojeCoolIme @@ -398,7 +398,7 @@ Nevažeći podaci URL je nevažeći Greška - Ukloni titlove iz titlova + Ukloni CC iz titlova Ukloni reklame iz titlova Filtriraj po željenom jeziku medija Extras @@ -446,7 +446,7 @@ Zapis Audio zapis Video zapis - Primijeni na ponovnom pokretanju + Primjenjuje se na ponovnom pokretanju Sigurnosni način rada omogućen Sve su ekstenzije isključene zbog rušenja aplikacije kako biste lakše pronašli ono koje uzrokuje probleme. Pogledajte podatke o padu @@ -472,7 +472,7 @@ Svi jezici Previše teksta. Nije moguće spremiti u međuspremnik. Označi kao gledano - Prikaži skočne prozore za preskakanje za početak/završetak + Prikazuje skočni prozor za preskakanje početka ili završetka medija Da Preuzimanje ažuriranja aplikacije… Jeste li sigurni da želite izaći\? @@ -495,7 +495,7 @@ Geste Značajke playera Titlovi - Respored + Raspored Zadane postavke Izgled Značajke @@ -527,7 +527,7 @@ Odaberite biblioteku Otvori sa Vaša je biblioteka prazna :( -\nPrijavite se na račun biblioteke ili dodajte emisije u svoju lokalnu biblioteku. +\nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. Ova je lista prazna. Pokušajte se prebaciti na jednu drugu listu. Pronađena datoteka sigurnog načina rada! \nNe učitavaju se ekstenzije pri pokretanju dok se datoteka ne ukloni. @@ -577,4 +577,6 @@ \nImat će kombinirani prioritet videozapisa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! + Već si glasao/la + Učestalost rezervne kopije diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index aaa65897..d6d5b7f6 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -132,7 +132,7 @@ העדכון התחיל מידע התקן אוטומטית את כל התוספים שטרם הותקנו ממאגרים שנוספו. - חפש אוטומטית עדכונים חדשים בפתיחת האפליקציה + חפש אוטומטית עדכונים חדשים לאחר פתיחת האפליקציה. הצג עדכונים לאפליקציה בצע מחדש את תהליך ההגדרה עדכן למהדורות מוקדמות @@ -146,7 +146,7 @@ לא נמצאו פרקים מחק קובץ מחק - עצור + השהה המשך -30 +30 @@ -176,7 +176,7 @@ אל תראה שוב דלג על עדכון זה עדכון - איכות צפייה מועדפת + איכות צפייה מועדפת (WiFi) נגן וידאו כותרת מקסימום תווים רזולוציית נגן וידאו הוסף מעקב @@ -184,7 +184,7 @@ הבא ספריה מטא-דאטה לא מסופק על ידי האתר, טעינת הסרטון תיכשל אם הוא לא קיים באתר. - החלק על הצד השמאלי או הימני כדי לשנות את הבהירות או עוצמת הקול + החלק בצד השמאלי או הימני כדי לשנות את הבהירות או עוצמת הקול שחזור הנתונים מהקובץ נכשל %s עדכונים וגיבויים נותן לך את תוצאות החיפוש מופרדות לפי ספק @@ -194,7 +194,7 @@ כמות בנינים שניתנו עונה מצטערים, האפליקציה קרסה. דוח באג אנונימי יישלח למפתחים - טורנט + טורנטים NSFW שגיאת מעבד הורד מראה @@ -226,8 +226,8 @@ תכונות נגן עדכן התקדמות צפייה DNS מעל HTTPS - לחץ באמצע כדי לעצור - החלק שמאלה או ימינה כדי לשלוט על זמן בנגן הסרטונים + לחץ פעמיים באמצע כדי לעצור + החלק מצד לצד כדי לשלוט על מיקומך בסרטון נגן אוטומטית את הפרק הבא התחל את הפרק הבא כאשר הפרק הנוכחי נגמר לחץ פעמיים על צד ימין או שמאל כדי להציץ קדימה או אחורה @@ -264,11 +264,11 @@ איכות מצלמה החלק כדי להציץ גיבוי - כמות הצצת הנגן + כמות הזזת הנגן (שניות) סרטון גיטהאב מצלמה - החלק לשינוי ההגדרות + החלק כדי לשנות הגדרות ‪דפדפן צבע חלון הצג לוג @@ -438,7 +438,7 @@ מיין בחר ספרייה נראה שהספרייה שלכם ריקה :( -\nהתחברו לחשבון ספריה או הוסף סדרות לספרייה המקומית שלך +\nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם. קובץ מצב בטוח נמצא! \nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. לא ניתן להתקין את הגרסה החדשה של האפליקציה @@ -505,5 +505,52 @@ אלפביתי (א \'עד ת\') אלפביתי (ת\' עד א\') פתח עם - נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת + נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת. + רשום ל%s + מעדכן סדרות שנרשמת אליהן + נגן מוסתר - כמות הזזה + רשת סלולארית + רשום + חזור + הרשמה ל-%s מבוטלת + פרוקסי עבור raw.githubusercontent.com + פרופילים + הממשק לא נוצר נכון. זוהי שגיאה רצינית, נא לדווח עליה מיידית ל-%s + עריכה + Wi-Fi + רקע הפרופיל + @string/default_subtitles + רשומה + עזרה + התחלה + עצור + פרופיל %d + הזמן שיזוז כשהנגן מוצג + נגן מוצג - כמות הזזה + אתחול + עוקף את החסימה של GitHub באמצעות jsDelivr. עלול לגרום לעדכונים להתעכב בכמה ימים. + בחר מצב כדי לסנן תוספים להורדה + איכויות + השתמש + בדיקת ספק + נכשל + פרק %d שוחרר לצפיה! + ביטול + איכות צפיה מועדפת (אינטרנט סלולארי) + המאגר לא נמצא, נא לבדוק את ה-URL ולנסות VPN + ההתחברות אל GitHub נכשלה. מדליק פרוקסי אל jsDelivr… + הזמן שיזוז כשהנגן מוסתר + כבר הצבעת + לא נמצאו תוספים במאגר + טלוויזיית אנדרואיד + קביעה כברירת מחדל + עבר + מעקף ספק אינטרנט + כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו. +\n +\nמקור א: 3 +\nאיכות ב: 7 +\nיגרמו לעדיפות הסרטון להיות 10. +\n +\nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index af4ea695..7f10aad4 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -218,4 +218,5 @@ フォントサイズ プロバイダーから探す 言語の自動選択 + リンクの読み込みエラー diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml new file mode 100644 index 00000000..a702a62f --- /dev/null +++ b/app/src/main/res/values-lt/strings.xml @@ -0,0 +1,265 @@ + + + Ištrinti failą + Pašalinti + Greitis (%.2fx) + Žymos + %d %s + Siuntimas pradėtas + Sekantis atsitiktinai + Peržiūros fonas + Paleisti filmą + Išjungti automatini klaidų pateikimą + Atstatyti į numatytą reikšmę + Išvalyti + Plakatas + Filtruoti žymas + Nepavyko įrašyti naujos programos versijos + Automatiškai ieškoti atnaujinimų, kai paleidžiama programa. + Filmas + Torrentas + Rodyti tarpines serijas animei + Šrifto dydis + Nuorodos ne rastos + Planuojama žiūrėti + Atnaujinimas pradėtas + Kopijuoti + Duoti bananai + Daugiau informacijos + Atsiuntimai + Automatiškai pasirinkti kalbą + Klaida kraunant nuorodas + +30 + Atsiuntimas baigtas + Tęsti žiūrėjimą + Rastas atnaujinimas! +\n%s -> %s + Atsisiųsti kalbas + Ieškoti naudojant tiekėjus + Grįžti atgal + Siunčiama + Daugiau pasirinkčiu + Paleisti seriją + Grotuvo greitis + %d Bananai duoti kūrėjams + Žiūrima + Pauzė + Žanrai + -30 + Serijos plakatas + Gali reikėti VPN šitam tiekėjui, kad veiktų teisingai + Ieškoti %s… + Github + Nėra duotu bananų + Subtitrų kalba + Keisti teikėją + Fono spalva + Ieškoti… + Nėra Sezonų + Leisti seriją + Paskyros + Sustabdyti siuntimą + Įgarsinimas + Subtitrų nustatymai + Lango spalva + Dokumentika + Transliuoti Torrentą + Ištrinti + Pradėti + Prideda greičio pasirinkti grotuve + Filmukas + Atsiuntimas atšauktas + Išplėstinė paieška + Tuščias sąrašas. Pabandykite pasirinkti kitą sąrašą. + Chromecast subtitrai + Įvertinimas: %.1f + Slėpti + Pritaikyti + Naršyklė + Subtitrai + Kita serija + Siuntimas sustabdytas + Rodyti programos atnaujinimus + Atsisiųsti + Biblioteka + Kelią problemas jei nustatytas per didelis ant prietaisų su mažai turymos vietos, tokiu kaip Android TV. + Kita + Ištrinti failą + Atsisiųsta + Pratęsti siuntimą + Azijietiškos dramos + Serija + Jūsų biblioteka tuščia :( +\nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos. + Pradėti sekančia seriją, kai dabartinė baigsis + Teksto spalva + Užbaigta + Naudoti sistemos ryškumą programos grotuve vietoj tamsumo + Tuščia + Nepavyko atstatyti duomenis iš failo %s + Paleisti anonsa + Paleisti gyva transliacija + Nerasta serijų + Šis tiekėjas yra iš Torrentų, VPN rekomenduojama naudoti + Nepavyko + Plakatas + Paleisti failą + Peržiūrima + Atidaryti Naršyklėje + Naudoti sistemos ryškumą + Atšaukti + Nėra duomenų + Šriftas + Perdaryti nustatymo procesą + %d-%d + Duoti bananą kūrėjams + Sugryšti + Nuoroda nukopijuota į iškarpinę + Paieška + Informacija + Praleisti įkėlima + Informacija + Serija %d bus išleista + Išsaugoti + Perdaug teksto. Nepavyko išsaugoti i iškarpynę. + Atsiuntimas nepavyko + Pasidalinti + Pagrindinis Plakatas + Šaltiniai + Nustatymai + Ieškoti + Kraunama… + Pašalinti + Daugiau informacijos + Sustabdyta + CloudStream + Paleisti + Gyvos transliacijos + APK įrašymas + Vidinė atmintis + Automatiškai paleisti kitą seriją + Grotuvo subtitrų nustatymai + Programos kalba + Uždaryti + Nustatyti žiūrėjimo statusą + Paleisti su CloudStream + Subtitrų iškėlimas + Serijos + Skleisti: %s + Sezonas + Pridėta %s + Šaltinis + Programos atnaujinimai + Nerasta atnaujinimų + Pavadinimas + Atsiuntimo kelias + Normalu + Klonuoti puslapį + Atnaujinimas + Atsisiųsti subtitrus + Ne rodyti dar kartą + Praleisti OP + Gyva transliacija + /\?\? + Netikėta grotuvo klaida + Įvertinta + Praleisti šį atnaujinimą + Veiksmai + Kopijuoti nuorodą + Paleisti programoje + Sinchronizuoti + Paleisti naršyklėje + Pašalinti puslapį + Perkrauti nuorodos + Išjungti + %d / 10 + Nuorodos + Pritraukti + Sutalpinti į ekraną + Užrakinti + Atsiuntimo klaida, patikrinkite atminties leidimus + Android TV + Azijietiška drama + /%d + Šaltinio klaida + Viešas sąrašas + 127.0.0.1 + Atsiųsta %d %s + Praleisti %s + Pagal numatytuosius nustatymus „CloudStream“ neturi įdiegtų svetainių. Turite įdiegti svetaines iš saugyklų. +\n +\nDėl beprotiško DMCA reikalavimų, kurios atliko Sky UK Limited 🤮, negalime susieti saugyklos svetainės programoje. +\n +\nPrisijunkite prie mūsų Discord arba ieškokite internete. + Mobilūs duomenys + šaunusPrisijungimoVardas + Autoriai + Naršyklė + Visos kalbos + 4K + Pradėta siųsti %d %s… + Aprašymas + Saugus režimas įjungtas + HDR + Ne atsiųsta: %d + Kitas + Pasirinkti biblioteką + Ne + Kalbos kodas (lt) + Baigta + Išvalyti istoriją + VLC + Redaguoti + Wi-Fi + Greitai būs… + Taip + Pagalba + Programa nerasta + Išjungta: %d + Rūšiuoti pagal + Atidarymas + Slaptazodis123 + Atidaryti su + Kalba + SD + sveikas@pasauli.com + Naudoti + SDR + Pavyzdys.com + UHD + Dydis + Palaikoma + MPV + ManoŠaunusPuslapis + Anonsas + Istorija + Serija %d išleista! + Atsiunčiamas programos atnaujinimas… + Atsiųsta: %d + Klaida + Atsijungti + Rūšiuoti + Vaizdo takelis + Blu-ray + Programa bus atnaujinta išėjus + Pagrindinė kalba + Prisijungti + Pažymėti kaip žiūrima + Ką norite matyti + Įrašomas programos atnaujinimas… + Nepavyko įkelti %s + Įvertinimas: %s + Statusas + Pabaiga + Klaidingas URL + Versija + 18+ + %s (išjungta) + DVD + Integruotas grotuvas + Atsisiųskite sąrašą puslapiu jūs norite naudoti + %s Ser %d + Ar tikrai norite išeiti\? + Pašalinti iš žiūrimų + Garso takelis + diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 17eeb883..222bef61 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -37,4 +37,23 @@ Kelajuan (%.2fx) Poster Poster + Salin + Muat Turun + Genre + Cari %s… + Sunting + Carian… + Pelayar + Episod seterusnya + Muat Turun + Buka dengan + Padam Fail + Buka Dalam Pelayar + Batal + Tiada Data + Info + Simpan + Kongsi + Tetapan + Tutup diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 177f7ea1..63dba53d 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -158,4 +158,5 @@ %s ସନ୍ଧାନ କରିବା… ପରବର୍ତ୍ତୀ ଅଧ୍ୟାୟ କୌଣସି ତଥ୍ୟ ନାହିଁ + %s ଅ %d diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml index 30b6f4a1..8384187f 100644 --- a/app/src/main/res/values-pl/array.xml +++ b/app/src/main/res/values-pl/array.xml @@ -203,12 +203,15 @@ Normalny + Żółty mniszek lekarski Goździk różowy + Pomarańczowy Ciemnozielony Kasztanowaty Ciemnoniebieski Szary Biały + Fajny niebieski Brązowy Niebieski Czerwony @@ -223,12 +226,15 @@ Normal + DandelionYellow CarnationPink + Orange DarkGreen Maroon NavyBlue Grey White + CoolBlue Brown Blue Red diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bcd3fc0f..e7b85b3d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -90,7 +90,7 @@ Скорость проигрыватель Воспроизвести Эпизод %dд %dч %dм - %d мин. + %d мин Dub Sub Установите смотреть состояние @@ -200,7 +200,7 @@ Автоматически загружать еще не установленные плагины из добавленных репозиториев. Присоединится в Discord Бесплатно - %d мин. + %dm %d ч. %d мин. Фильмы Мультфильм @@ -529,4 +529,28 @@ Обход ограничения доступа к GitHub с помощью jsDelivr может задержать обновления на несколько дней. Подписные Отказались от подписки на %s + Мобильный интернет + Профили + Пользовательский интерфейс не был доступен для правильного создания, это ГЛАВНАЯ ОШИБКА и должна быть сообщена немедленно %s + Изменить + Интернет + Задний фон профиля + \@строка/обычные_субтитры + Помощь + Профиль %d + Выберите режим фильтера плагинов для загрузки + Качество + Использовать + Выключить + Хранилище не обнаружено, проверьте URL и попробуйте с ВПН + Вы уже проголосовали + Никаких плагинов не обнаружено + Поставить обычный + Здесь вы можете изменить порядок расположения источников. Если видео имеет более высокий приоритет, оно будет отображаться выше в списке источников. Сумма приоритета источника и приоритета качества составляет приоритет видео. +\n +\nИсточник А: 3 +\nКачество Б: 7 +\nБудет иметь общий приоритет видео 10. +\n +\nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 3f4134e5..9c9a335b 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,8 +1,8 @@ - தேடுக - தேடல் %s… - வேகம் + தேடுக… + %s இல் தேடவும்… + வேகம் (%.2fx) முகப்பு தேடு பதிவிறக்கம் @@ -60,7 +60,7 @@ தேடுவதற்கு இருமுறை தட்டவும் பிளேயரில் தேடுதல் வேகம் இடைநிறுத்துவதற்கு நடுவில் தட்டவும் - நடிகர்கள் + நடிகர்கள்: %s பின் செல் அமைப்புகள் ஏதும் இல்லை @@ -117,4 +117,9 @@ புதிய புதுப்பிப்பு உள்ளது \n%s->%s நிரப்பி + போஸ்டர் + எபிசோட்டின் போஸ்டர் + போஸ்டர் + பிரதான போஸ்டர் + %s Ep %d diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml index dca01736..d14a3e2a 100644 --- a/app/src/main/res/values-tr/array.xml +++ b/app/src/main/res/values-tr/array.xml @@ -229,12 +229,15 @@ Normal + Karahindiba sarı Karanfil Pembesi + Turuncu Koyu Yeşil Kestane Lacivert Gri Beyaz + Soğuk Mavi Kahverengi Soğuk Ateş @@ -249,12 +252,15 @@ Normal + DandelionYellow CarnationPink + Orange DarkGreen Maroon NavyBlue Grey White + CoolBlue Brown Blue Red diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index eef9bc95..e0d7e4cd 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -594,4 +594,6 @@ \nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! Kaliteler Profil arkaplanı + UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s + Select mode to filter plugins download diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f9dccfc4..c069cae0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -38,7 +38,7 @@ Переглянути фільм Переглянути трейлер Трансляція через торент - Повторити підключення… + Повторити з\'єднання… Назад Переглянути епізод Завантажено @@ -136,7 +136,7 @@ Крок перемотки (секунди) Натисніть двічі посередині, щоб призупинити відтворення відео Використовувати яскравість системи - Оновити прогрес перегляду + Оновлювати прогрес перегляду Відновлення даних з резервної копії Резервне копіювання даних Не вдалося відновити дані з файлу %s @@ -193,7 +193,7 @@ Телесеріали Мультфільми Аніме - ОВА + OVA Азіатські драми Прямі трансляції Інше @@ -206,7 +206,7 @@ Відео Помилка джерела Віддалена помилка - Помилка рендеринга + Помилка рендерингу Дзеркало Chromecast Переглянути в застосунку Переглянути в %s @@ -214,7 +214,7 @@ Завантажити дзеркало Перевірити наявність оновлень Заблокувати - Пропускати OP + Пропустити ОП Не показувати знову Оновити Бажана якість перегляду (WiFi) @@ -233,7 +233,7 @@ Автоматично шукає нові оновлення після запуску застосунку. Оновлення до бета-версій Посилання скопійовано в буфер обміну - Деякі телефони не підтримують новий інсталятор пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. + Деякі телефони не підтримують новий встановлювач пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. Приєднуйтесь до Discord Дано бананів Рік @@ -553,4 +553,5 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували + Частота резервного копіювання diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 48b73efa..b9585d07 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -28,12 +28,12 @@ مزید آپشنز اگلا قسط براؤزر میں کھولیں - لوڈ کرنا سکیپ کر دیں + لوڈنگ چھوڑیں لوڈ ہو رہا ہے… - دیکھنا - آن ہولڈ - گرا دیا - دیکھنا ہے + دیکھ رہے ہیں + معطل + چھوڑ دیا گیا + دیکھنے کا منصوبہ کوئی نہیں دوبارہ دیکھنا مووی لگائے @@ -95,7 +95,7 @@ کھلا (آن) یہ سورس Torrent ہے، ضرورت پڑنے پر VPN کا استعمال کریں تفصیل - کوئی تفصیل نہیں ملی + کوئی کہانی نہیں ملی کوئی تفصیل نہیں ملی غلطی کا لاگ دیکھیں 🐈 PIP @@ -134,14 +134,14 @@ ایپ کی تازہ کاریاں نمایش کریں سیٹ اپ کا عمل دوبارہ کریں پری ریلیز کے لیے اپ ڈیٹ کریں - صرف مکمل ریلیز کے بجائے پری ریلیز اپ ڈیٹس تلاش کریں + صرف مکمل جاریات کی بجائے پیش رلیز اپ ڈیٹس کی تلاش کریں APK انسٹالر Github - ایک ہی dev کی طرف سے light ناول اپلی کیشن - اسی devs کے ذریعے anime ایپ + یہ لائٹ ناول ایپ وہی ڈویلپرز نے تیار کی ہے جو اس ایپ کو ڈویلپ کیا ہے + یہ اینمی ایپ وہی ڈویلپرز نے تیار کی ہے جو اس ایپ کو ڈویلپ کیا ہے ڈسکارڈ میں شامل ہوں - دیووں کو ایک بینین دیں - دی گئی بینین + شکریہ ڈویلپرز کو! آپ کا کام شاندار ہے + تعریفیں ایپ کی زبان اس فراہم کنندہ کے پاس کروم کاسٹ سپورٹ نہیں ہے کوئی لنکس نہیں ملے @@ -150,7 +150,7 @@ طے شدہ قدر پر ری سیٹ کریں %s قسط %d پوسٹر - شیئر + شئیر کریں انواع رفتار (%.2fx) مکمل @@ -178,7 +178,7 @@ anime کے لیے فلر ایپیسوڈ دکھائیں پلگ ان خود بخود ڈاؤن لوڈ کریں شامل کردہ ذخیروں سے خود بخود تمام ابھی تک انسٹال نہیں ہوئے پلگ ان انسٹال کریں۔ - شروع ہونے پر خودکار طور پر نئی اپ ڈیٹس تلاش کریں + اپلیکیشن کو شروع کرنے کے بعد خود بخود نئی اپ ڈیٹس کی تلاش کریں۔ کچھ فون نئے پیکیج انسٹالر کو سپورٹ نہیں کرتے ہیں. اگر اپ ڈیٹس انسٹال نہیں ہوتے ہیں تو لیگیسی آپشن کو آزمائیں. معذرت، ایپلی کیشن کریش ہو گئی. ایک گمنام بگ رپورٹ ڈویلپرز کو بھیجی جائے گی سیزن @@ -233,7 +233,7 @@ اووا ٹورینٹ دستاویزی فلم - ایشیائی ڈرامے + ایشین ڈرامہ لائیو اسٹریمز ویڈیو ماخذ نقص @@ -333,15 +333,15 @@ سایہ ذیلی ہم وقت سازی کریں 1000 ms - سب ٹائٹل تاخیر + زیرنویس میں دیری اگر سب ٹائٹلز %d ms بہت جلد دکھائے جائیں تو اسے استعمال کریں - سب ٹائٹل تاخیر + کوئی زیرنویس میں دیری نہیں این ایس ایف ڈبلیو آٹو ڈاؤن لوڈ براؤزر میں چلائیں کم میموری والے آلات، جیسے کہ Android TV پر بہت زیادہ سیٹ ہونے پر کریشوں کا سبب بنتا ہے. سائٹ ہٹائیں - ایک مختلف URL کے ساتھ ، موجودہ سائٹ کا کلون شامل کریں. + ایک مختلف URL کے ساتھ ، موجودہ سائٹ کا کلون شامل کریں اسکرین پر فٹ کھینچیں اکاؤنٹ بنائیں @@ -488,7 +488,7 @@ غلط ID مخزن کا نام ختم ہونے والا - کھل رہا + کھولنا کیا آپ کو یقین ہے کہ آپ ہیاں سے نکلنا چاہتے ہیں؟ ایس ڈی تمام زبانیں @@ -519,12 +519,38 @@ سب ٹائٹلز سے بند کیپشنز کو ہٹا دیں اپنے آلے کے مطابق ایپ کی شکل تبدیل کریں اگلے - CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ -\nSky UK Limited 🤮 کی طرف سے بے دماغ DMCA ہٹانے کی وجہ سے ہم ایپ میں ریپوزٹری سائٹ کو لنک نہیں کر سکتے۔ -\nہمارے ڈسکارڈ میں شامل ہوں یا آن لائن تلاش کریں۔ + CloudStream کے ساتھ پہلے سے کوئی ویب سائٹس انسٹال نہیں ہیں۔ آپ کو مخزنوں سے ویب سائٹس انسٹال کرنی ہوں گی۔ +\n +\nSky UK Limited کی طرف سے DMCA کے تعلیم نامے کی بنا پر 🤮، ہم ایپ کے اندر مخزن سائٹ کی مستقیم لنک فراہم نہیں کر سکتے۔ +\n +\nبراہ کرم ہمارے ڈسکارڈ کمیونٹی میں شامل ہونے کو مد نظر رکھیں یا مخزن کیلئے آن لائن تلاش کریں۔ تمام ایکسٹینشنز کریش کی وجہ سے آف کر دی گئیں تاکہ آپ کو پریشانی کا باعث تلاش کرنے میں مدد مل سکے۔ پہلے ایکسٹینشن انسٹال کریں بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ ایپ اپ ڈیٹ انسٹال ہو رہا ہے… یہ فہرست خالی ہے۔ کسی اور پر سوئچ کرنے کی کوشش کریں۔ + موبائل ڈیٹا + پروفائلز + یو آئی درست طریقے سے تخلیق نہیں کی جاسکتی تھی، یہ ایک بڑا بگ ہے اور فوراً رپورٹ کیا جانا چاہئے%s + ترتیب دیں + وائی فائی + پروفائل پس منظر + @string/default_subtitles + مدد + پروفائل %d + پلگ انز کو ڈاؤن لوڈ کرنے کے لئے موڈ منتخب کریں + کوالٹیز + استعمال کریں + غیرفعال کریں + مخزن نہیں ملا، URL کو چیک کریں اور VPN استعمال کریں + آپ نے پہلے ہی ووٹ دیا ہے + مخزن میں کوئی پلگ انز نہیں ملا + ترجیحی تعین کریں + یہاں آپ تبدیلی کرسکتے ہیں کہ سورسز کو کس طرح کی ترتیب دی جائے۔ اگر ایک ویڈیو کی زیادہ پرائیورٹی ہوتی ہے تو یہ سورس کی انتخاب میں زیادہ اوپر آئے گی۔ سورس کی پرائیورٹی اور کوالٹی کی پرائیورٹی کا مجموعہ ویڈیو کی پرائیورٹی ہوتی ہے۔ +\n +\nسورس A: 3 +\nکوالٹی B: 7 +\nاس کا مجموعی ویڈیو پرائیورٹی 10 ہوتی ہے۔ +\n +\nنوٹ: اگر مجموعہ 10 یا اس سے زیادہ ہو تو پلیر وہ لنک لوڈ کرنے کو خود بخود چھوڑ دے گا! diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml index a5145c9e..d32f37ce 100644 --- a/app/src/main/res/values-vi/array.xml +++ b/app/src/main/res/values-vi/array.xml @@ -195,12 +195,15 @@ Mặc định + bồ công anh màu vàng Hồng nhạt + Quả cam Xanh lam đậm Nâu sẫm Xanh lục Xám Trắng + Màu xanh mát Nâu Xanh lục nhạt Đỏ @@ -215,12 +218,15 @@ Normal + DandelionYellow CarnationPink + Orange DarkGreen Maroon NavyBlue Grey White + CoolBlue Brown Blue Red diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 1df7b9d6..e38dd5c9 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -41,7 +41,7 @@ 0 1 - + @string/disable @@ -126,6 +126,26 @@ 30min + + @string/none + 3h + 6h + 12h + 24h + 3d + 7d + + + + 0 + 3 + 6 + 12 + 24 + 72 + 168 + + 0 60 @@ -246,12 +266,15 @@ Normal + Dandelion Yellow Carnation Pink + Orange Dark Green Maroon Navy Blue Grey White + Cool Blue Brown Cool Fire @@ -266,12 +289,15 @@ Normal + DandelionYellow CarnationPink + Orange DarkGreen Maroon NavyBlue Grey White + CoolBlue Brown Blue Red diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d9258c40..c2c84d0d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -81,6 +81,9 @@ #515151 #FFFFFF #622C00 + #CE8500 + #F5BB00 + #408cac #48E484 #ea596e diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b44706c1..b349aecc 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -19,5 +19,5 @@ 62dp 50dp - + 1dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13251c7c..c722f33f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ primary_color_key restore_key backup_key + automatic_backup_key prefer_media_type_key_2 app_theme_key episode_sync_enabled_key @@ -229,6 +230,7 @@ Automatically sync your current episode progress Restore data from backup Back up data + Backup frequency Loaded backup file Failed to restore data from file %s Data stored diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e2f11221..2fa4eb41 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -86,8 +86,8 @@ @@ -352,6 +353,36 @@ @color/colorPrimaryBrown + + + + + + @@ -500,9 +531,8 @@ 20dp 20dp true - + @drawable/outline_drawable_less + ?attr/selectableItemBackgroundBorderless ?attr/textColor ?android:attr/textAppearanceListItemSmall @drawable/ic_baseline_keyboard_arrow_right_24 diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml index 9989e47b..e3b36648 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -9,7 +9,7 @@ android:summaryOn="@string/bug_report_settings_on" android:title="@string/pref_disable_acra" /> - - + + - - + -