diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bce60cfa..97c04bcb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -61,16 +61,18 @@ android { } } - compileSdk = 33 + compileSdk = 34 buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + + // https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading + targetSdk = 33 // android 14 is fucked versionCode = 62 - versionName = "4.2.0" + versionName = "4.2.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -175,28 +177,30 @@ 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.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0 + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") - // dont change this to 1.6.0 it looks ugly af - implementation("com.google.android.material:material:1.5.0") + implementation("com.google.android.material:material:1.10.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - 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") + + implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") + implementation("androidx.navigation:navigation-ui-ktx:2.7.4") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + 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") + implementation("androidx.preference:preference-ktx:1.2.1") implementation("com.github.bumptech.glide:glide:4.13.1") kapt("com.github.bumptech.glide:compiler:4.13.1") @@ -220,28 +224,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") @@ -250,18 +252,18 @@ dependencies { // Networking // implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") - implementation("com.github.Blatzar:NiceHttp:0.4.3") + implementation("com.github.Blatzar:NiceHttp:0.4.4") // http library // 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") @@ -273,7 +275,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 @@ -298,6 +300,8 @@ dependencies { group = "org.apache.httpcomponents", ) } + // seekbar https://github.com/rubensousa/PreviewSeekBar + implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") } diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index df41ef91..faacdf50 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 @@ -17,6 +19,7 @@ import com.lagradost.cloudstream3.databinding.FragmentSearchBinding import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomepageParentBinding +import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding @@ -117,9 +120,12 @@ class ExampleInstrumentedTest { // testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv) // testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv) - 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.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent) + testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent) + testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, 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 088e16db..5e50dbc4 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"> @@ -92,12 +96,6 @@ android:launchMode="singleTask" android:resizeableActivity="true" android:supportsPictureInPicture="true"> - - - - - - @@ -172,6 +170,22 @@ + + + + + + + + + + + + @@ -186,6 +200,7 @@ @@ -195,6 +210,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 5b674c4c..35a628a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1246,6 +1246,18 @@ interface LoadResponse { return this.syncData[aniListIdPrefix] } + fun LoadResponse.getImdbId(): String? { + return normalSafeApiCall { + SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb) + } + } + + fun LoadResponse.getTMDbId(): String? { + return normalSafeApiCall { + SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb) + } + } + fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) @@ -1453,6 +1465,15 @@ interface EpisodeResponse { var nextAiring: NextAiring? var seasonNames: List? fun getLatestEpisodes(): Map + + /** Count all episodes in all previous seasons up until this episode to get a total count. + * Example: + * Season 1: 10 episodes. + * Season 2: 6 episodes. + * + * getTotalEpisodeIndex(episode = 3, season = 2) -> 10 + 3 = 13 + * */ + fun getTotalEpisodeIndex(episode: Int, season: Int): Int } @JvmName("addSeasonNamesString") @@ -1532,6 +1553,12 @@ data class AnimeLoadResponse( .takeUnless { it == Int.MIN_VALUE } }.toMap() } + + override fun getTotalEpisodeIndex(episode: Int, season: Int): Int { + return this.episodes.maxOf { (_, episodes) -> + episodes.count { ((it.season ?: Int.MIN_VALUE) < season) && it.season != 0 } + } + episode + } } /** @@ -1740,6 +1767,12 @@ data class TvSeriesLoadResponse( .takeUnless { it == Int.MIN_VALUE } return mapOf(DubStatus.None to max) } + + override fun getTotalEpisodeIndex(episode: Int, season: Int): Int { + return episodes.count { + (it.season ?: Int.MIN_VALUE) < season && it.season != 0 + } + episode + } } suspend fun MainAPI.newTvSeriesLoadResponse( diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 4a9fbd62..b2f364cb 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 @@ -283,6 +284,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 /** @@ -544,13 +546,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 + //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } @@ -592,6 +594,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { override fun onResume() { super.onResume() afterPluginsLoadedEvent += ::onAllPluginsLoaded + setActivityInstance(this) try { if (isCastApiAvailable()) { //mCastSession = mSessionManager.currentCastSession @@ -661,7 +664,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val isAtHome = navController?.currentDestination?.matchDestination(R.id.navigation_home) == true - if (isAtHome && isTrueTvSettings()) { + if (isAtHome && isTvSettings()) { showConfirmExitDialog() } else { super.onBackPressed() @@ -1072,7 +1075,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) @@ -1112,7 +1130,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) normalSafeApiCall { - backup() + backup(this) } normalSafeApiCall { // Recompile oat on new version @@ -1126,38 +1144,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) @@ -1303,7 +1310,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it]) + viewModel.updateWatchStatus(WatchType.values()[it], this@MainActivity) } } 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/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/metaproviders/MultiAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt deleted file mode 100644 index 8cfe1e9a..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.lagradost.cloudstream3.metaproviders - -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId -import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.SyncAPI -import com.lagradost.cloudstream3.syncproviders.providers.AniListApi -import com.lagradost.cloudstream3.syncproviders.providers.MALApi -import com.lagradost.cloudstream3.utils.SyncUtil - -// wont be implemented -class MultiAnimeProvider : MainAPI() { - override var name = "MultiAnime" - override var lang = "en" - override val usesWebView = true - override val supportedTypes = setOf(TvType.Anime) - private val syncApi: SyncAPI = aniListApi - - private val syncUtilType by lazy { - when (syncApi) { - is AniListApi -> "anilist" - is MALApi -> "myanimelist" - else -> throw ErrorLoadingException("Invalid Api") - } - } - - private val validApis - get() = - synchronized(APIHolder.apis) { - APIHolder.apis.filter { - it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains( - TvType.Anime - ) - } - } - - - private fun filterName(name: String): String { - return Regex("""[^a-zA-Z0-9-]""").replace(name, "") - } - - override suspend fun search(query: String): List? { - return syncApi.search(query)?.map { - AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl) - } - } - - override suspend fun load(url: String): LoadResponse? { - return syncApi.getResult(url)?.let { res -> - val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url -> - validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url) - }.filterNotNull() - - val type = - if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime - - newAnimeLoadResponse( - res.title ?: throw ErrorLoadingException("No Title found"), - url, - type - ) { - posterUrl = res.posterUrl - plot = res.synopsis - tags = res.genres - rating = res.publicScore - addTrailer(res.trailers) - addAniListId(res.id.toIntOrNull()) - recommendations = res.recommendations - } - } - } -} \ No newline at end of file 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 5bb96ed1..8e87cc99 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -477,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/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index e6ca9711..99723e90 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -8,7 +8,9 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.Coroutines.ioWork +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData @@ -69,29 +71,52 @@ class LocalList : SyncAPI { }?.distinctBy { it.first } ?: return null val list = ioWork { - watchStatusIds.groupBy { - it.second.stringRes - }.mapValues { group -> + val isTrueTv = isTrueTvSettings() + + val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { + // None is not something to display + it.stringRes to emptyList() + } + mapOf( + R.string.favorites_list_name to emptyList() + ) + if (!isTrueTv) { + mapOf( + R.string.subscription_list_name to emptyList() + ) + } else { + emptyMap() + } + + val watchStatusMap = watchStatusIds.groupBy { it.second.stringRes }.mapValues { group -> group.value.mapNotNull { getBookmarkedData(it.first)?.toLibraryItem(it.first.toString()) } - } + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull { + } + + val favoritesMap = mapOf(R.string.favorites_list_name to getAllFavorites().mapNotNull { it.toLibraryItem() }) + + // Don't show subscriptions on TV + val result = if (isTrueTv) { + baseMap + watchStatusMap + favoritesMap + } else { + val subscriptionsMap = mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull { + it.toLibraryItem() + }) + + baseMap + watchStatusMap + subscriptionsMap + favoritesMap + } + + result } - val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { - // None is not something to display - it.stringRes to emptyList() - } + mapOf(R.string.subscription_list_name to emptyList()) - return SyncAPI.LibraryMetadata( - (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, + list.map { SyncAPI.LibraryList(txt(it.key), it.value) }, setOf( ListSorting.AlphabeticalA, ListSorting.AlphabeticalZ, -// ListSorting.UpdatedNew, -// ListSorting.UpdatedOld, + ListSorting.UpdatedNew, + ListSorting.UpdatedOld, // ListSorting.RatingHigh, // ListSorting.RatingLow, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index cd1df562..3a37a228 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -203,7 +203,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } /** Read the id string to get all other ids */ - private fun readIdFromString(idString: String?): Map { + fun readIdFromString(idString: String?): Map { return tryParseJson(idString) ?: return emptyMap() } @@ -376,6 +376,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { private var status: Int? = null, private var addEpisodes: Pair?, List?>? = null, private var removeEpisodes: Pair?, List?>? = null, + // Required for knowing if the status should be overwritten + private var onList: Boolean = false ) { fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor } fun apiUrl(url: String) = apply { this.url = url } @@ -387,6 +389,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } fun status(newStatus: Int?, oldStatus: Int?) = apply { + onList = oldStatus != null // Only set status if its new if (newStatus != oldStatus) { this.status = newStatus @@ -412,6 +415,11 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { // Do not add episodes if there is no change if (newEpisodes > (oldEpisodes ?: 0)) { this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes)) + + // Set to watching if episodes are added and there is no current status + if (!onList) { + status = SimklListStatusType.Watching.value + } } if ((oldEpisodes ?: 0) > newEpisodes) { this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes)) @@ -431,6 +439,28 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { interceptor = interceptor ).isSuccessful } else { + val statusResponse = status?.let { setStatus -> + val newStatus = + SimklListStatusType.values() + .firstOrNull { it.value == setStatus }?.originalName + ?: SimklListStatusType.Watching.originalName!! + + app.post( + "${this.url}/sync/add-to-list", + json = StatusRequest( + shows = listOf( + StatusMediaObject( + null, + null, + ids, + newStatus, + ) + ), movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } ?: true + val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) -> app.post( "${this.url}/sync/history/remove", @@ -472,28 +502,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { true } - val statusResponse = status?.let { setStatus -> - val newStatus = - SimklListStatusType.values() - .firstOrNull { it.value == setStatus }?.originalName - ?: SimklListStatusType.Watching.originalName!! - - app.post( - "${this.url}/sync/add-to-list", - json = StatusRequest( - shows = listOf( - StatusMediaObject( - null, - null, - ids, - newStatus, - ) - ), movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } ?: true - statusResponse && episodeRemovalResponse && historyResponse } } @@ -1051,4 +1059,4 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { return true } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt index 6b3090a9..c5c38dc0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt @@ -55,7 +55,6 @@ class WhoIsWatchingAdapter( editCallBack = editCallBack, ) - override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) = holder.bind(currentList.getOrNull(position)) @@ -70,10 +69,15 @@ class WhoIsWatchingAdapter( fun bind(card: DataStoreHelper.Account?) { when (binding) { is WhoIsWatchingAccountBinding -> binding.apply { - if(card == null) return@apply + if (card == null) return@apply outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex profileText.text = card.name profileImageBackground.setImage(card.image) + + // Handle the lock indicator + val isLocked = card.lockPin != null + lockIcon.isVisible = isLocked + root.setOnClickListener { selectCallBack(card) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt new file mode 100644 index 00000000..72551199 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt @@ -0,0 +1,64 @@ +package com.lagradost.cloudstream3.ui.account + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.databinding.AccountListItemBinding +import com.lagradost.cloudstream3.ui.result.setImage +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.DataStoreHelper + +class AccountAdapter( + private val accounts: List, + private val onItemClick: (DataStoreHelper.Account) -> Unit +) : RecyclerView.Adapter() { + + inner class AccountViewHolder(private val binding: AccountListItemBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(account: DataStoreHelper.Account) { + val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex + + binding.accountName.text = account.name + binding.accountImage.setImage(account.image) + binding.lockIcon.isVisible = account.lockPin != null + binding.outline.isVisible = isLastUsedAccount + + if (isTvSettings()) { + binding.root.isFocusableInTouchMode = true + if (isLastUsedAccount) { + binding.root.requestFocus() + } + } + + binding.root.setOnClickListener { + onItemClick(account) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { + val binding = AccountListItemBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + + if (isTvSettings()) { + val layoutParams = binding.root.layoutParams as RecyclerView.LayoutParams + val marginInDp = 5 // Set the margin to 5dp + val marginInPixels = (marginInDp * parent.resources.displayMetrics.density).toInt() + layoutParams.setMargins(marginInPixels, marginInPixels, marginInPixels, marginInPixels) + binding.root.layoutParams = layoutParams + } + + return AccountViewHolder(binding) + } + + override fun onBindViewHolder(holder: AccountViewHolder, position: Int) { + holder.bind(accounts[position]) + } + + override fun getItemCount(): Int { + return accounts.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountDialog.kt new file mode 100644 index 00000000..dfd8831b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountDialog.kt @@ -0,0 +1,115 @@ +package com.lagradost.cloudstream3.ui.account + +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.LockPinDialogBinding +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe + +object AccountDialog { + // TODO add account creation dialog to allow creating accounts directly from AccountSelectActivity + + fun showPinInputDialog( + context: Context, + currentPin: String?, + editAccount: Boolean, + callback: (String?) -> Unit + ) { + fun TextView.visibleWithText(@StringRes textRes: Int) { + visibility = View.VISIBLE + setText(textRes) + } + + fun View.isVisible() = visibility == View.VISIBLE + + val binding = LockPinDialogBinding.inflate(LayoutInflater.from(context)) + + val isPinSet = currentPin != null + val isNewPin = editAccount && !isPinSet + val isEditPin = editAccount && isPinSet + + val titleRes = if (isEditPin) R.string.enter_current_pin else R.string.enter_pin + + val dialog = AlertDialog.Builder(context, R.style.AlertDialogCustom) + .setView(binding.root) + .setTitle(titleRes) + .setNegativeButton(R.string.cancel) { _, _ -> + callback.invoke(null) + } + .setOnCancelListener { + callback.invoke(null) + } + .setOnDismissListener { + if (binding.pinEditTextError.isVisible()) { + callback.invoke(null) + } + } + .create() + + var isPinValid = false + + binding.pinEditText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + val enteredPin = s.toString() + val isEnteredPinValid = enteredPin.length == 4 + + if (isEnteredPinValid) { + if (isPinSet) { + if (enteredPin != currentPin) { + binding.pinEditTextError.visibleWithText(R.string.pin_error_incorrect) + binding.pinEditText.text = null + isPinValid = false + } else { + binding.pinEditTextError.visibility = View.GONE + isPinValid = true + + callback.invoke(enteredPin) + dialog.dismissSafe() + } + } else { + binding.pinEditTextError.visibility = View.GONE + isPinValid = true + } + } else if (isNewPin) { + binding.pinEditTextError.visibleWithText(R.string.pin_error_length) + isPinValid = false + } + } + + override fun afterTextChanged(s: Editable?) {} + }) + + // Detect IME_ACTION_DONE + binding.pinEditText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE && isPinValid) { + val enteredPin = binding.pinEditText.text.toString() + callback.invoke(enteredPin) + dialog.dismissSafe() + } + true + } + + // We don't want to accidentally have the dialog dismiss when clicking outside of it. + // That is what the cancel button is for. + dialog.setCanceledOnTouchOutside(false) + + dialog.show() + + // Auto focus on PIN input and show keyboard + binding.pinEditText.requestFocus() + binding.pinEditText.postDelayed({ + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(binding.pinEditText, InputMethodManager.SHOW_IMPLICIT) + }, 200) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt new file mode 100644 index 00000000..a2c34bf0 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -0,0 +1,92 @@ +package com.lagradost.cloudstream3.ui.account + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.CommonActivity +import com.lagradost.cloudstream3.CommonActivity.loadThemes +import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ActivityAccountSelectBinding +import com.lagradost.cloudstream3.databinding.ActivityAccountSelectTvBinding +import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAccounts +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute + +class AccountSelectActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val accounts = getAccounts(this@AccountSelectActivity) + + // Don't show account selection if there is only + // one account that exists + if (accounts.count() <= 1) { + navigateToMainActivity() + return + } + + CommonActivity.init(this) + loadThemes(this) + + window.navigationBarColor = colorFromAttribute(R.attr.primaryBlackBackground) + + val binding = if (isTvSettings()) { + ActivityAccountSelectTvBinding.inflate(layoutInflater) + } else ActivityAccountSelectBinding.inflate(layoutInflater) + + setContentView(binding.root) + + val recyclerView: RecyclerView = binding.root.findViewById(R.id.account_recycler_view) + + + val adapter = AccountAdapter(accounts) { selectedAccount -> + // Handle the selected account + onAccountSelected(selectedAccount) + } + recyclerView.adapter = adapter + + recyclerView.layoutManager = if (isTvSettings()) { + LinearLayoutManager(this) + } else GridLayoutManager(this, 2) + } + + private fun onAccountSelected(selectedAccount: DataStoreHelper.Account) { + if (selectedAccount.lockPin != null) { + // The selected account has a PIN set, prompt the user to enter the PIN + showPinInputDialog(this@AccountSelectActivity, selectedAccount.lockPin, false) { pin -> + if (pin == null) return@showPinInputDialog + // Pin is correct, proceed to main activity + setAccount(selectedAccount) + navigateToMainActivity() + } + } else { + // No PIN set for the selected account, proceed to main activity + setAccount(selectedAccount) + navigateToMainActivity() + } + } + + private fun setAccount(account: DataStoreHelper.Account) { + // Don't reload if it is the same account + if (DataStoreHelper.selectedKeyIndex == account.keyIndex) { + return + } + + DataStoreHelper.selectedKeyIndex = account.keyIndex + + MainActivity.bookmarksUpdatedEvent(true) + MainActivity.reloadHomeEvent(true) + } + + private fun navigateToMainActivity() { + val mainIntent = Intent(this, MainActivity::class.java) + startActivity(mainIntent) + finish() // Finish the account selection activity + } +} \ 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 3e6ba6ab..541b128f 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 @@ -27,7 +27,6 @@ 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.afterBackupRestoreEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -55,8 +54,6 @@ 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.DataStoreHelper.currentHomePage import com.lagradost.cloudstream3.utils.Event @@ -67,9 +64,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes 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() @@ -371,10 +365,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() @@ -402,7 +393,7 @@ class HomeFragment : Fragment() { } fun updateList() { - this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes) + DataStoreHelper.homePreference = preSelectedTypes arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 163a60a1..443278a9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -15,7 +15,8 @@ import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable class LoadClickCallback( @@ -34,11 +35,13 @@ open class ParentItemAdapter( ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val root = LayoutInflater.from(parent.context).inflate( - if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent, - parent, - false - ) + val layoutResId = when { + isTrueTvSettings() -> R.layout.homepage_parent_tv + parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator + else -> R.layout.homepage_parent + } + + val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false) val binding = HomepageParentBinding.bind(root) @@ -234,7 +237,7 @@ open class ParentItemAdapter( }) //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() - if (!isTvSettings()) { + if (!isTrueTvSettings()) { title.setOnClickListener { moreInfoClickCallback.invoke(expand) } 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 d7956f39..5f194f1f 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 @@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SearchClickCallback +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog @@ -81,6 +82,28 @@ class HomeParentItemAdapterPreview( parent, false ) else FragmentHomeHeadBinding.inflate(inflater, parent, false) + + if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) { + binding.homeBookmarkParentItemMoreInfo.isVisible = true + + val marginInDp = 50 + val density = binding.horizontalScrollChips.context.resources.displayMetrics.density + val marginInPixels = (marginInDp * density).toInt() + + val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams + params.marginEnd = marginInPixels + binding.horizontalScrollChips.layoutParams = params + binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds( + null, + null, + ContextCompat.getDrawable( + parent.context, + R.drawable.ic_baseline_arrow_forward_24 + ), + null + ) + } + HeaderViewHolder( binding, viewModel, @@ -355,21 +378,25 @@ class HomeParentItemAdapterPreview( showApply = false, {}) { val newValue = WatchType.values()[it] - homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds( - null, - ContextCompat.getDrawable( - homePreviewBookmark.context, - newValue.iconRes - ), - null, - null - ) - homePreviewBookmark.setText(newValue.stringRes) - ResultViewModel2.updateWatchStatus( - item, - newValue - ) + ResultViewModel2().updateWatchStatus( + newValue, + fab.context, + item + ) { statusChanged: Boolean -> + if (!statusChanged) return@updateWatchStatus + + homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds( + null, + ContextCompat.getDrawable( + homePreviewBookmark.context, + newValue.iconRes + ), + null, + null + ) + homePreviewBookmark.setText(newValue.stringRes) + } } } } @@ -553,12 +580,19 @@ class HomeParentItemAdapterPreview( resumeHolder.isVisible = resumeWatching.isNotEmpty() resumeAdapter.updateList(resumeWatching) - if (binding is FragmentHomeHeadBinding) { - binding.homeWatchParentItemTitle.setOnClickListener { + if ( + binding is FragmentHomeHeadBinding || + binding is FragmentHomeHeadTvBinding && + binding.root.context.isEmulatorSettings() + ) { + val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle + ?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle + + title?.setOnClickListener { viewModel.popup( HomeViewModel.ExpandableHomepageList( HomePageList( - binding.homeWatchParentItemTitle.text.toString(), + title.text.toString(), resumeWatching, false ), 1, false @@ -576,8 +610,15 @@ class HomeParentItemAdapterPreview( bookmarkHolder.isVisible = visible bookmarkAdapter.updateList(list) - if (binding is FragmentHomeHeadBinding) { - binding.homeBookmarkParentItemTitle.setOnClickListener { + if ( + binding is FragmentHomeHeadBinding || + binding is FragmentHomeHeadTvBinding && + binding.root.context.isEmulatorSettings() + ) { + val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle + ?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle + + title?.setOnClickListener { val items = toggleList.map { it.first }.filter { it.isChecked } if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog val textSum = items 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 13d34b59..f471fefd 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 @@ -41,7 +40,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds @@ -103,11 +101,6 @@ class HomeViewModel : ViewModel() { loadStoredData() } - fun deleteBookmarks() { - deleteAllBookmarkedData() - loadStoredData() - } - var repo: APIRepository? = null private val _apiName = MutableLiveData() @@ -170,10 +163,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 @@ -181,16 +171,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) } @@ -463,7 +451,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) 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 ab2af7ce..85e7468a 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,5 +1,6 @@ package com.lagradost.cloudstream3.ui.library +import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.res.Configuration @@ -8,14 +9,25 @@ import android.os.Handler import android.os.Looper import android.util.Log import androidx.fragment.app.Fragment +import android.util.TypedValue 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.ImageView +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.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 @@ -23,21 +35,26 @@ 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.MainActivity +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.BackupAPI 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 @@ -48,7 +65,7 @@ const val LIBRARY_FOLDER = "library_folder" enum class LibraryOpenerType(@StringRes val stringRes: Int) { - Default(R.string.default_subtitles), // TODO FIX AFTER MERGE + Default(R.string.action_default), Provider(R.string.none), Browser(R.string.browser), Search(R.string.search), @@ -83,9 +100,21 @@ class LibraryFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { MainActivity.afterBackupRestoreEvent += ::onNewSyncData - 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) } @@ -103,26 +132,25 @@ 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" } + // Set the color for the search exit icon to the correct theme text color + val searchExitIcon = binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) + val searchExitIconColor = TypedValue() + + activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true) + searchExitIcon?.setColorFilter(searchExitIconColor.data) + binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { libraryViewModel.sort(ListSorting.Query, query) @@ -186,7 +214,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 @@ -221,7 +249,7 @@ class LibraryFragment : Fragment() { } setKey( - LIBRARY_FOLDER, + "$currentAccount/$LIBRARY_FOLDER", key, savedData, ) @@ -234,6 +262,7 @@ class LibraryFragment : Fragment() { } binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) + binding?.viewpager?.adapter = binding?.viewpager?.adapter ?: ViewpagerAdapter( mutableListOf(), @@ -268,8 +297,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 @@ -357,11 +389,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: @@ -398,6 +437,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 @@ -420,8 +462,25 @@ 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) @@ -440,6 +499,21 @@ class LibraryFragment : Fragment() { private fun onNewSyncData(unused: Unit) { Log.d(BackupAPI.LOG_KEY, "will reload pages") libraryViewModel.reloadPages(true) + + 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) + }) } } 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 8388e58f..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 @@ -535,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 331cfb73..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 @@ -56,6 +57,7 @@ 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 @@ -88,7 +90,9 @@ class CS3IPlayer : IPlayer { 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!" }) + debugAssert( + { field != null && value != null }, + { "Previous player instance should be released!" }) field = value } @@ -96,6 +100,8 @@ class CS3IPlayer : IPlayer { var simpleCacheSize = 0L var videoBufferMs = 0L + val imageGenerator = IPreviewGenerator.new() + private val seekActionTime = 30000L private var ignoreSSL: Boolean = true @@ -182,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, @@ -190,7 +204,8 @@ class CS3IPlayer : IPlayer { startPosition: Long?, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? + autoPlay: Boolean?, + preview: Boolean, ) { Log.i(TAG, "loadPlayer") if (sameEpisode) { @@ -209,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) { @@ -494,6 +528,7 @@ class CS3IPlayer : IPlayer { } override fun release() { + imageGenerator.release() releasePlayer() } @@ -508,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 } @@ -871,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 -> 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 cdb184a2..f98c9b25 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 @@ -182,6 +182,7 @@ class GeneratorPlayer : FullScreenPlayer() { (if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle( currentSubs, settings = true, downloads = true ), + preview = isFullScreenPlayer ) } @@ -1025,7 +1026,7 @@ class GeneratorPlayer : FullScreenPlayer() { ctx.getString(R.string.episode_sync_enabled_key), true ) ) maxEpisodeSet = meta.episode - sync.modifyMaxEpisode(meta.episode) + sync.modifyMaxEpisode(meta.totalEpisodeIndex ?: meta.episode) } } 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/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 53c7c2fa..5b300c06 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 @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.content.res.Configuration import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -215,10 +216,16 @@ class QuickSearchFragment : Fragment() { binding?.quickSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) //val searchMagIcon = - // binding.quickSearch.findViewById(androidx.appcompat.R.id.search_mag_icon) + // binding?.quickSearch?.findViewById(androidx.appcompat.R.id.search_mag_icon) - //searchMagIcon?.scaleX = 0.65f - //searchMagIcon?.scaleY = 0.65f + // searchMagIcon?.scaleX = 0.65f + // searchMagIcon?.scaleY = 0.65f + + // Set the color for the search exit icon to the correct theme text color + val searchExitIconColor = TypedValue() + + activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true) + searchExitIcon?.setColorFilter(searchExitIconColor.data) binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 7617bc11..a1574eec 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -47,7 +47,9 @@ data class ResultEpisode( /** * Conveys if the episode itself is marked as watched **/ - val videoWatchState: VideoWatchState + val videoWatchState: VideoWatchState, + /** Sum of all previous season episode counts + episode */ + val totalEpisodeIndex: Int? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -82,6 +84,7 @@ fun buildResultEpisode( isFiller: Boolean? = null, tvType: TvType, parentId: Int, + totalEpisodeIndex: Int? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -103,7 +106,8 @@ fun buildResultEpisode( isFiller, tvType, parentId, - videoWatchState + videoWatchState, + totalEpisodeIndex ) } 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..7bcce764 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 @@ -17,7 +17,6 @@ import android.view.animation.DecelerateInterpolator import android.widget.AbsListView import android.widget.ArrayAdapter import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView @@ -66,6 +65,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute @@ -151,7 +151,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { startPosition = 0L, subtitles = emptySet(), subtitle = null, - autoPlay = false + autoPlay = false, + preview = false ) true } ?: run { @@ -429,20 +430,36 @@ open class ResultFragmentPhone : FullScreenPlayer() { } }) resultSubscribe.setOnClickListener { - val isSubscribed = - viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener + viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleSubscriptionStatus - val message = if (isSubscribed) { - // Kinda icky to have this here, but it works. - SubscriptionWorkManager.enqueuePeriodicWork(context) - R.string.subscription_new - } else { - R.string.subscription_deleted + val message = if (newStatus) { + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } + } + resultFavorite.setOnClickListener { + viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleFavoriteStatus - val name = (viewModel.page.value as? Resource.Success)?.value?.title - ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + val message = if (newStatus) { + R.string.favorite_added + } else { + R.string.favorite_removed + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + } } mediaRouteButton.apply { val chromecastSupport = api?.hasChromecastSupport == true @@ -563,6 +580,19 @@ open class ResultFragmentPhone : FullScreenPlayer() { binding?.resultSubscribe?.setImageResource(drawable) } + observeNullable(viewModel.favoriteStatus) { isFavorite -> + binding?.resultFavorite?.isVisible = isFavorite != null + if (isFavorite == null) return@observeNullable + + val drawable = if (isFavorite) { + R.drawable.ic_baseline_favorite_24 + } else { + R.drawable.ic_baseline_favorite_border_24 + } + + binding?.resultFavorite?.setImageResource(drawable) + } + observe(viewModel.trailers) { trailers -> setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet! } @@ -653,14 +683,13 @@ open class ResultFragmentPhone : FullScreenPlayer() { resultPoster.setImage(d.posterImage) resultPosterBackground.setImage(d.posterBackgroundImage) resultDescription.setTextHtml(d.plotText) - resultDescription.setOnClickListener { view -> - // todo bottom view? - view.context?.let { ctx -> - val builder: AlertDialog.Builder = - AlertDialog.Builder(ctx, R.style.AlertDialogCustom) - builder.setMessage(d.plotText.asString(ctx).html()) - .setTitle(d.plotHeaderText.asString(ctx)) - .show() + resultDescription.setOnClickListener { + activity?.let { activity -> + activity.showBottomDialogText( + d.titleText.asString(activity), + d.plotText.asString(activity).html(), + {} + ) } } @@ -851,16 +880,11 @@ open class ResultFragmentPhone : FullScreenPlayer() { setRecommendations(recommendations, null) } observe(viewModel.episodeSynopsis) { description -> - // TODO bottom dialog - view.context?.let { ctx -> - val builder: AlertDialog.Builder = - AlertDialog.Builder(ctx, R.style.AlertDialogCustom) - builder.setMessage(description.html()) - .setTitle(R.string.synopsis) - .setOnDismissListener { - viewModel.releaseEpisodeSynopsis() - } - .show() + activity?.let { activity -> + activity.showBottomDialogText( + activity.getString(R.string.synopsis), + description.html() + ) { viewModel.releaseEpisodeSynopsis() } } } context?.let { ctx -> @@ -938,7 +962,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it]) + viewModel.updateWatchStatus(WatchType.values()[it], context) } } } 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 7c784a43..396ec863 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 @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.DecelerateInterpolator +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible @@ -17,6 +18,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder.updateHasTrailers +import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -26,6 +28,7 @@ import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable +import com.lagradost.cloudstream3.services.SubscriptionWorkManager import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator @@ -35,6 +38,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isRtl @@ -216,11 +220,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 } } } @@ -247,7 +249,7 @@ class ResultFragmentTv : Fragment() { binding?.apply { //episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f - + val leftListener: View.OnFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> if (!hasFocus) return@OnFocusChangeListener @@ -267,6 +269,7 @@ class ResultFragmentTv : Fragment() { resultEpisodesShow.onFocusChangeListener = rightListener resultDescription.onFocusChangeListener = leftListener resultBookmarkButton.onFocusChangeListener = leftListener + resultFavoriteButton.onFocusChangeListener = leftListener resultEpisodesShow.setOnClickListener { // toggle, to make it more touch accessable just in case someone thinks that a // tv layout is better but is using a touch device @@ -285,7 +288,9 @@ class ResultFragmentTv : Fragment() { resultPlaySeries, resultResumeSeries, resultPlayTrailer, - resultBookmarkButton + resultBookmarkButton, + resultFavoriteButton, + resultSubscribeButton ) for (requestView in views) { if (!requestView.isVisible) continue @@ -426,6 +431,8 @@ class ResultFragmentTv : Fragment() { val aboveCast = listOf( binding?.resultEpisodesShow, binding?.resultBookmarkButton, + binding?.resultFavoriteButton, + binding?.resultSubscribeButton, ).firstOrNull { it?.isVisible == true } @@ -528,7 +535,83 @@ class ResultFragmentTv : Fragment() { view.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it]) + viewModel.updateWatchStatus(WatchType.values()[it], context) + } + } + } + } + + observeNullable(viewModel.favoriteStatus) { isFavorite -> + binding?.resultFavoriteButton?.apply { + isVisible = isFavorite != null + if (isFavorite == null) return@observeNullable + + val drawable = if (isFavorite) { + R.drawable.ic_baseline_favorite_24 + } else { + R.drawable.ic_baseline_favorite_border_24 + } + + val text = if (isFavorite) { + R.string.action_remove_from_favorites + } else { + R.string.action_add_to_favorites + } + + setIconResource(drawable) + setText(text) + setOnClickListener { + viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleFavoriteStatus + + val message = if (newStatus) { + R.string.favorite_added + } else { + R.string.favorite_removed + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + } + } + } + } + + observeNullable(viewModel.subscribeStatus) { isSubscribed -> + binding?.resultSubscribeButton?.apply { + isVisible = isSubscribed != null && context.isEmulatorSettings() + if (isSubscribed == null) return@observeNullable + + val drawable = if (isSubscribed) { + R.drawable.ic_baseline_notifications_active_24 + } else { + R.drawable.baseline_notifications_none_24 + } + + val text = if (isSubscribed) { + R.string.action_unsubscribe + } else { + R.string.action_subscribe + } + + setIconResource(drawable) + setText(text) + setOnClickListener { + viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleSubscriptionStatus + + val message = if (newStatus) { + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } } } @@ -537,6 +620,7 @@ class ResultFragmentTv : Fragment() { observeNullable(viewModel.movie) { data -> binding?.apply { resultPlayMovie.isVisible = data is Resource.Success + resultPlaySeries.isVisible = data == null seriesHolder.isVisible = data == null resultEpisodesShow.isVisible = data == null @@ -766,12 +850,14 @@ class ResultFragmentTv : Fragment() { R.drawable.profile_bg_red, R.drawable.profile_bg_teal ).random() + //Change poster crop area to 20% from Top + backgroundPoster.cropYCenterOffsetPct = 0.20F + backgroundPoster.setImage( d.posterBackgroundImage ?: UiImage.Drawable(error), radius = 0, errorImageDrawable = error ) - resultComingSoon.isVisible = d.comingSoon resultDataHolder.isGone = d.comingSoon UIHelper.populateChips(resultTag, d.tags) 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 6acf476a..d744fac5 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 @@ -7,6 +7,8 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.widget.Toast +import androidx.annotation.MainThread +import androidx.appcompat.app.AlertDialog import androidx.core.content.FileProvider import androidx.core.net.toUri import androidx.lifecycle.LiveData @@ -31,6 +33,7 @@ import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.providers.Kitsu +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO @@ -45,19 +48,37 @@ import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe import com.lagradost.cloudstream3.utils.Coroutines.main +import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions +import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub +import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState +import com.lagradost.cloudstream3.utils.DataStoreHelper.getSubscribedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos +import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData +import com.lagradost.cloudstream3.utils.DataStoreHelper.removeSubscribedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub +import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason +import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState +import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState +import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.UIHelper.navigate import kotlinx.coroutines.* import java.io.File @@ -110,6 +131,18 @@ data class ResultData( val plotHeaderText: UiText, ) +data class CheckDuplicateData( + val name: String, + val year: Int?, + val syncData: Map? +) + +enum class LibraryListType { + BOOKMARKS, + FAVORITES, + SUBSCRIPTIONS +} + fun txt(status: DubStatus?): UiText? { return txt( when (status) { @@ -425,6 +458,9 @@ class ResultViewModel2 : ViewModel() { private val _subscribeStatus: MutableLiveData = MutableLiveData(null) val subscribeStatus: LiveData = _subscribeStatus + private val _favoriteStatus: MutableLiveData = MutableLiveData(null) + val favoriteStatus: LiveData = _favoriteStatus + companion object { const val TAG = "RVM2" //private const val EPISODE_RANGE_SIZE = 20 @@ -435,33 +471,6 @@ class ResultViewModel2 : ViewModel() { return this?.firstOrNull { it.season == season } } - fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) { - val currentId = currentResponse.getId() - - val currentWatchType = getResultWatchState(currentId) - - DataStoreHelper.setResultWatchState(currentId, status.internalId) - val current = DataStoreHelper.getBookmarkedData(currentId) - val currentTime = System.currentTimeMillis() - DataStoreHelper.setBookmarkedData( - currentId, - DataStoreHelper.BookmarkedData( - currentId, - current?.bookmarkedTime ?: currentTime, - currentTime, - currentResponse.name, - currentResponse.url, - currentResponse.apiName, - currentResponse.type, - currentResponse.posterUrl, - currentResponse.year - ) - ) - if (currentWatchType != status) { - MainActivity.bookmarksUpdatedEvent(true) - } - } - private fun filterName(name: String?): String? { if (name == null) return null Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let { @@ -816,9 +825,77 @@ class ResultViewModel2 : ViewModel() { val selectPopup: LiveData = _selectPopup - fun updateWatchStatus(status: WatchType) { - updateWatchStatus(currentResponse ?: return, status) - _watchStatus.postValue(status) + fun updateWatchStatus( + status: WatchType, + context: Context?, + loadResponse: LoadResponse? = null, + statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null + ) { + val response = loadResponse ?: currentResponse ?: return + + val currentId = response.getId() + + val currentStatus = getResultWatchState(currentId) + + // If the current status is "NONE" and the new status is not "NONE", + // fetch the bookmarked data to check for duplicates, otherwise set this + // to an empty list, so that we don't show the duplicate warning dialog, + // but we still want to update the current bookmark and refresh the data anyway. + val bookmarkedData = if (currentStatus == WatchType.NONE && status != WatchType.NONE) { + getAllBookmarkedData() + } else emptyList() + + checkAndWarnDuplicates( + context, + LibraryListType.BOOKMARKS, + CheckDuplicateData( + name = response.name, + year = response.year, + syncData = response.syncData, + ), + bookmarkedData + ) { shouldContinue: Boolean, duplicateIds: List -> + if (!shouldContinue) return@checkAndWarnDuplicates + + if (duplicateIds.isNotEmpty()) { + duplicateIds.forEach { duplicateId -> + deleteBookmarkedData(duplicateId) + } + } + + setResultWatchState(currentId, status.internalId) + + // We don't need to store if WatchType.NONE. + // The key is removed in setResultWatchState, we don't want to + // re-add it again here if it was just removed. + if (status != WatchType.NONE) { + val current = getBookmarkedData(currentId) + + setBookmarkedData( + currentId, + DataStoreHelper.BookmarkedData( + current?.bookmarkedTime ?: unixTimeMS, + currentId, + unixTimeMS, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl, + response.year, + response.syncData + ) + ) + } + + if (currentStatus != status) { + MainActivity.bookmarksUpdatedEvent(true) + } + + _watchStatus.postValue(status) + + statusChangedCallback?.invoke(true) + } } private fun startChromecast( @@ -833,39 +910,255 @@ class ResultViewModel2 : ViewModel() { } /** - * @return true if the new status is Subscribed, false if not. Null if not possible to subscribe. - **/ - fun toggleSubscriptionStatus(): Boolean? { - val isSubscribed = _subscribeStatus.value ?: return null - val response = currentResponse ?: return null - if (response !is EpisodeResponse) return null + * Toggles the subscription status of an item. + * + * @param context The context to use for operations. + * @param statusChangedCallback A callback that is invoked when the subscription status changes. + * It provides the new subscription status (true if subscribed, false if unsubscribed, null if action was canceled). + */ + fun toggleSubscriptionStatus( + context: Context?, + statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null + ) { + val isSubscribed = _subscribeStatus.value ?: return + val response = currentResponse ?: return + if (response !is EpisodeResponse) return val currentId = response.getId() if (isSubscribed) { - DataStoreHelper.removeSubscribedData(currentId) + removeSubscribedData(currentId) + statusChangedCallback?.invoke(false) + _subscribeStatus.postValue(false) } else { - val current = DataStoreHelper.getSubscribedData(currentId) + checkAndWarnDuplicates( + context, + LibraryListType.SUBSCRIPTIONS, + CheckDuplicateData( + name = response.name, + year = response.year, + syncData = response.syncData, + ), + getAllSubscriptions(), + ) { shouldContinue: Boolean, duplicateIds: List -> + if (!shouldContinue) { + statusChangedCallback?.invoke(null) + return@checkAndWarnDuplicates + } - DataStoreHelper.setSubscribedData( - currentId, - DataStoreHelper.SubscribedData( + if (duplicateIds.isNotEmpty()) { + duplicateIds.forEach { duplicateId -> + removeSubscribedData(duplicateId) + } + } + + val current = getSubscribedData(currentId) + + setSubscribedData( currentId, - current?.bookmarkedTime ?: unixTimeMS, - unixTimeMS, - response.getLatestEpisodes(), - response.name, - response.url, - response.apiName, - response.type, - response.posterUrl, - response.year + DataStoreHelper.SubscribedData( + current?.subscribedTime ?: unixTimeMS, + response.getLatestEpisodes(), + currentId, + unixTimeMS, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl, + response.year, + response.syncData + ) ) - ) + + _subscribeStatus.postValue(true) + + statusChangedCallback?.invoke(true) + } + } + } + + /** + * Toggles the favorite status of an item. + * + * @param context The context to use. + * @param statusChangedCallback A callback that is invoked when the favorite status changes. + * It provides the new favorite status (true if added to favorites, false if removed, null if action was canceled). + */ + fun toggleFavoriteStatus( + context: Context?, + statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null + ) { + val isFavorite = _favoriteStatus.value ?: return + val response = currentResponse ?: return + + val currentId = response.getId() + + if (isFavorite) { + removeFavoritesData(currentId) + statusChangedCallback?.invoke(false) + _favoriteStatus.postValue(false) + } else { + checkAndWarnDuplicates( + context, + LibraryListType.FAVORITES, + CheckDuplicateData( + name = response.name, + year = response.year, + syncData = response.syncData, + ), + getAllFavorites(), + ) { shouldContinue: Boolean, duplicateIds: List -> + if (!shouldContinue) { + statusChangedCallback?.invoke(null) + return@checkAndWarnDuplicates + } + + if (duplicateIds.isNotEmpty()) { + duplicateIds.forEach { duplicateId -> + removeFavoritesData(duplicateId) + } + } + + val current = getFavoritesData(currentId) + + setFavoritesData( + currentId, + DataStoreHelper.FavoritesData( + current?.favoritesTime ?: unixTimeMS, + currentId, + unixTimeMS, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl, + response.year, + response.syncData + ) + ) + + _favoriteStatus.postValue(true) + + statusChangedCallback?.invoke(true) + } + } + } + + @MainThread + private fun checkAndWarnDuplicates( + context: Context?, + listType: LibraryListType, + checkDuplicateData: CheckDuplicateData, + data: List, + checkDuplicatesCallback: (shouldContinue: Boolean, duplicateIds: List) -> Unit + ) { + val whitespaceRegex = "\\s+".toRegex() + fun normalizeString(input: String): String { + /** + * Trim the input string and replace consecutive spaces with a single space. + * This covers some edge-cases where the title does not match exactly across providers, + * and one provider has the title with an extra whitespace. This is minor enough that + * it should still match in this case. + */ + return input.trim().replace(whitespaceRegex, " ") } - _subscribeStatus.postValue(!isSubscribed) - return !isSubscribed + val syncData = checkDuplicateData.syncData + + val imdbId = getImdbIdFromSyncData(syncData) + val tmdbId = getTMDbIdFromSyncData(syncData) + val malId = syncData?.get(AccountManager.malApi.idPrefix) + val aniListId = syncData?.get(AccountManager.aniListApi.idPrefix) + val normalizedName = normalizeString(checkDuplicateData.name) + val year = checkDuplicateData.year + + val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> + val librarySyncData = it.syncData + + val checks = listOf( + { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, + { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, + { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, + { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, + { normalizedName == normalizeString(it.name) && year == it.year } + ) + + checks.any { it() } + } + + if (duplicateEntries.isEmpty() || context == null) { + checkDuplicatesCallback.invoke(true, emptyList()) + return + } + + val replaceMessage = if (duplicateEntries.size > 1) { + R.string.duplicate_replace_all + } else R.string.duplicate_replace + + val message = if (duplicateEntries.size == 1) { + val list = when (listType) { + LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes + LibraryListType.FAVORITES -> R.string.favorites_list_name + LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name + } + + context.getString(R.string.duplicate_message_single, + "${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}" + ) + } else { + val bulletPoints = duplicateEntries.joinToString("\n") { + val list = when (listType) { + LibraryListType.BOOKMARKS -> getResultWatchState(it.id ?: 0).stringRes + LibraryListType.FAVORITES -> R.string.favorites_list_name + LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name + } + + "• ${it.apiName}: ${normalizeString(it.name)} (${context.getString(list)})" + } + + context.getString(R.string.duplicate_message_multiple, bulletPoints) + } + + val builder: AlertDialog.Builder = AlertDialog.Builder(context) + + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + checkDuplicatesCallback.invoke(true, emptyList()) + } + DialogInterface.BUTTON_NEGATIVE -> { + checkDuplicatesCallback.invoke(false, emptyList()) + } + DialogInterface.BUTTON_NEUTRAL -> { + checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id }) + } + } + } + + builder.setTitle(R.string.duplicate_title) + .setMessage(message) + .setPositiveButton(R.string.duplicate_add, dialogClickListener) + .setNegativeButton(R.string.duplicate_cancel, dialogClickListener) + .setNeutralButton(replaceMessage, dialogClickListener) + .show().setDefaultFocus() + } + + private fun getImdbIdFromSyncData(syncData: Map?): String? { + return normalSafeApiCall { + SimklApi.readIdFromString( + syncData?.get(AccountManager.simklApi.idPrefix) + )[SimklApi.Companion.SyncServices.Imdb] + } + } + + private fun getTMDbIdFromSyncData(syncData: Map?): String? { + return normalSafeApiCall { + SimklApi.readIdFromString( + syncData?.get(AccountManager.simklApi.idPrefix) + )[SimklApi.Companion.SyncServices.Tmdb] + } } private fun startChromecast( @@ -1219,7 +1512,7 @@ class ResultViewModel2 : ViewModel() { // Do not add mark as watched on movies if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) { val isWatched = - DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched + getVideoWatchState(click.data.id) == VideoWatchState.Watched val watchedText = if (isWatched) R.string.action_remove_from_watched else R.string.action_mark_as_watched @@ -1468,12 +1761,12 @@ class ResultViewModel2 : ViewModel() { ACTION_MARK_AS_WATCHED -> { val isWatched = - DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched + getVideoWatchState(click.data.id) == VideoWatchState.Watched if (isWatched) { - DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.None) + setVideoWatchState(click.data.id, VideoWatchState.None) } else { - DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.Watched) + setVideoWatchState(click.data.id, VideoWatchState.Watched) } // Kinda dirty to reload all episodes :( @@ -1682,7 +1975,7 @@ class ResultViewModel2 : ViewModel() { list.subList(start, end).map { val posDur = getViewPos(it.id) val watchState = - DataStoreHelper.getVideoWatchState(it.id) ?: VideoWatchState.None + getVideoWatchState(it.id) ?: VideoWatchState.None it.copy( position = posDur?.position ?: 0, duration = posDur?.duration ?: 0, @@ -1743,13 +2036,19 @@ class ResultViewModel2 : ViewModel() { private fun postSubscription(loadResponse: LoadResponse) { if (loadResponse.isEpisodeBased()) { val id = loadResponse.getId() - val data = DataStoreHelper.getSubscribedData(id) - DataStoreHelper.updateSubscribedData(id, data, loadResponse as? EpisodeResponse) + val data = getSubscribedData(id) + updateSubscribedData(id, data, loadResponse as? EpisodeResponse) val isSubscribed = data != null _subscribeStatus.postValue(isSubscribed) } } + private fun postFavorites(loadResponse: LoadResponse) { + val id = loadResponse.getId() + val isFavorite = getFavoritesData(id) != null + _favoriteStatus.postValue(isFavorite) + } + private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) { if (range == null || indexer == null) { return @@ -1887,6 +2186,7 @@ class ResultViewModel2 : ViewModel() { currentResponse = loadResponse postPage(loadResponse, apiRepository) postSubscription(loadResponse) + postFavorites(loadResponse) if (updateEpisodes) postEpisodes(loadResponse, updateFillers) } @@ -1915,6 +2215,10 @@ class ResultViewModel2 : ViewModel() { val id = mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) ?: 0) + + val totalIndex = + i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) } + if (!existingEpisodes.contains(id)) { existingEpisodes.add(id) val seasonData = loadResponse.seasonNames.getSeason(i.season) @@ -1934,7 +2238,8 @@ class ResultViewModel2 : ViewModel() { i.description, fillers.getOrDefault(episode, false), loadResponse.type, - mainId + mainId, + totalIndex ) val season = eps.seasonIndex ?: 0 @@ -1963,6 +2268,9 @@ class ResultViewModel2 : ViewModel() { val seasonData = loadResponse.seasonNames.getSeason(episode.season) + val totalIndex = + episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) } + val ep = buildResultEpisode( loadResponse.name, @@ -1979,7 +2287,8 @@ class ResultViewModel2 : ViewModel() { episode.description, null, loadResponse.type, - mainId + mainId, + totalIndex ) val season = ep.seasonIndex ?: 0 @@ -2010,7 +2319,8 @@ class ResultViewModel2 : ViewModel() { null, null, loadResponse.type, - mainId + mainId, + null ) ) } @@ -2032,7 +2342,8 @@ class ResultViewModel2 : ViewModel() { null, null, loadResponse.type, - mainId + mainId, + null ) ) } @@ -2054,7 +2365,8 @@ class ResultViewModel2 : ViewModel() { null, null, loadResponse.type, - mainId + mainId, + null ) ) } @@ -2115,13 +2427,13 @@ class ResultViewModel2 : ViewModel() { postResume() } - fun postResume() { + private fun postResume() { _resumeWatching.postValue(resume()) } private fun resume(): ResumeWatchingStatus? { val correctId = currentId ?: return null - val resume = DataStoreHelper.getLastWatched(correctId) + val resume = getLastWatched(correctId) val resumeParentId = resume?.parentId if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched val resumeId = resume.episodeId ?: return null// invalid episode id 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..65dfd679 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 @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.search import android.content.DialogInterface import android.content.res.Configuration import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -11,6 +12,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 +24,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 +60,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 +69,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 +197,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 +223,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 } @@ -228,17 +232,17 @@ class SearchFragment : Fragment() { val searchExitIcon = binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) // val searchMagIcon = - // main_search.findViewById(androidx.appcompat.R.id.search_mag_icon) - //searchMagIcon.scaleX = 0.65f - //searchMagIcon.scaleY = 0.65f + // binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_mag_icon) + // 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() - } + // Set the color for the search exit icon to the correct theme text color + val searchExitIconColor = TypedValue() + + activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true) + searchExitIcon?.setColorFilter(searchExitIconColor.data) + + selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet() binding?.searchFilter?.setOnClickListener { searchView -> searchView?.context?.let { ctx -> @@ -286,7 +290,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 +315,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 +341,7 @@ class SearchFragment : Fragment() { } dialog.setOnDismissListener { - context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList()) + DataStoreHelper.searchPreferenceProviders = currentSelectedApis.toList() selectedApis = currentSelectedApis } updateList(selectedSearchTypes.toList()) @@ -353,10 +352,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 +394,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 +506,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 +555,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 6a5a2f62..16efe9f1 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 @@ -194,13 +194,13 @@ class SettingsFragment : Fragment() { } binding?.apply { listOf( - settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general, - settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player, - settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account, - settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui, - settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers, - settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates, - settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions, + settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, + settingsPlayer to R.id.action_navigation_global_to_navigation_settings_player, + settingsCredits to R.id.action_navigation_global_to_navigation_settings_account, + settingsUi to R.id.action_navigation_global_to_navigation_settings_ui, + settingsProviders to R.id.action_navigation_global_to_navigation_settings_providers, + settingsUpdates to R.id.action_navigation_global_to_navigation_settings_updates, + settingsExtensions to R.id.action_navigation_global_to_navigation_settings_extensions, ).forEach { (view, navigationId) -> view.apply { setOnClickListener { @@ -235,4 +235,4 @@ class SettingsFragment : Fragment() { } } } -} \ 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 6ab21c7d..286cb96c 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 @@ -82,6 +82,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/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 91adfde0..6cb74437 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 @@ -20,15 +20,17 @@ import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener +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.DataStore.getSyncPrefs 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 @@ -50,7 +52,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 } @@ -67,7 +92,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() @@ -179,7 +204,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) @@ -189,7 +215,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/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 48917889..1be966b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -583,7 +583,7 @@ object AppUtils { //private val viewModel: ResultViewModel by activityViewModels() private fun getResultsId(): Int { - return if (isTrueTvSettings()) { + return if (isTvSettings()) { R.id.global_to_navigation_results_tv } else { R.id.global_to_navigation_results_phone 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 eea106c7..1a946b5e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -11,6 +11,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.MainActivity.Companion.afterBackupRestoreEvent import com.lagradost.cloudstream3.R @@ -150,10 +151,12 @@ object BackupUtils { } @Suppress("UNCHECKED_CAST") - fun Context.getBackup(): BackupFile { + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + val syncDataPrefs = getSyncPrefs().all.filter { it.key.isTransferable() } - val allData = getSharedPrefs().all.filter { it.key.isTransferable() } - val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() } + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } + val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } val syncData = BackupVars( syncDataPrefs.filter { it.value is Boolean } as? Map, @@ -226,21 +229,23 @@ object BackupUtils { } @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) 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 7bce1b6c..e687bcfb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -6,11 +6,14 @@ import android.text.Editable import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone +import androidx.core.view.isVisible 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,19 +27,23 @@ 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.result.FOCUS_SELF +import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog +import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState 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" const val RESULT_WATCH_STATE = "result_watch_state" const val RESULT_WATCH_STATE_DATA = "result_watch_state_data" const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data" +const val RESULT_FAVORITES_STATE_DATA = "result_favorites_state_data" const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching" const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated" @@ -44,6 +51,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 +85,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, @@ -65,6 +137,8 @@ object DataStoreHelper { val customImage: String? = null, @JsonProperty("defaultImageIndex") val defaultImageIndex: Int, + @JsonProperty("lockPin") + val lockPin: String? = null, ) { val image: UiImage get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable( @@ -131,7 +205,6 @@ object DataStoreHelper { // update UI setAccount(getDefaultAccount(context), true) - MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } @@ -160,36 +233,86 @@ object DataStoreHelper { binding.profilePic.setImage(account.image) binding.profilePic.setOnClickListener { - // rolls the image forwards once + // Roll the image forwards once currentEditAccount = currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size) binding.profilePic.setImage(currentEditAccount.image) } binding.applyBtt.setOnClickListener { - val currentAccounts = accounts.toMutableList() - - val overrideIndex = - currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex } - - // if an account is found that has the same keyIndex then override that one, if not then append it - if (overrideIndex != -1) { - currentAccounts[overrideIndex] = currentEditAccount + if (currentEditAccount.lockPin != null) { + // Ask for the current PIN + showPinInputDialog(context, currentEditAccount.lockPin, false) { pin -> + if (pin == null) return@showPinInputDialog + // PIN is correct, proceed to update the account + performAccountUpdate(currentEditAccount) + dialog.dismissSafe() + } } else { - currentAccounts.add(currentEditAccount) + // No lock PIN set, proceed to update the account + performAccountUpdate(currentEditAccount) + dialog.dismissSafe() } - - // 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, false) - DataStoreHelper.currentHomePage = currentHomePage - - accounts = currentAccounts.toTypedArray() - - dialog.dismissSafe() } + + // Handle setting or changing the PIN + + if (currentEditAccount.keyIndex == getDefaultAccount(context).keyIndex) { + binding.lockProfileCheckbox.isVisible = false + if (currentEditAccount.lockPin != null) { + currentEditAccount = currentEditAccount.copy(lockPin = null) + } + } + + var canSetPin = true + + binding.lockProfileCheckbox.isChecked = currentEditAccount.lockPin != null + + binding.lockProfileCheckbox.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + if (canSetPin) { + showPinInputDialog(context, null, true) { pin -> + if (pin == null) { + binding.lockProfileCheckbox.isChecked = false + return@showPinInputDialog + } + + currentEditAccount = currentEditAccount.copy(lockPin = pin) + } + } + } else { + if (currentEditAccount.lockPin != null) { + // Ask for the current PIN + showPinInputDialog(context, currentEditAccount.lockPin, true) { pin -> + if (pin == null || pin != currentEditAccount.lockPin) { + canSetPin = false + binding.lockProfileCheckbox.isChecked = true + } else { + currentEditAccount = currentEditAccount.copy(lockPin = null) + } + } + } + } + } + + canSetPin = true + } + + private fun performAccountUpdate(account: Account) { + val currentAccounts = accounts.toMutableList() + + val overrideIndex = currentAccounts.indexOfFirst { it.keyIndex == account.keyIndex } + + if (overrideIndex != -1) { + currentAccounts[overrideIndex] = account + } else { + currentAccounts.add(account) + } + + val currentHomePage = this.currentHomePage + setAccount(account, false) + this.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() } private fun getDefaultAccount(context: Context): Account { @@ -202,10 +325,18 @@ object DataStoreHelper { } } + fun getAccounts(context: Context): List { + return accounts.toMutableList().apply { + val item = getDefaultAccount(context) + remove(item) + add(0, item) + } + } + fun showWhoIsWatching(context: Context) { - val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate( - LayoutInflater.from(context) - ) + val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(LayoutInflater.from(context)) + val builder = BottomSheetDialog(context) + builder.setContentView(binding.root) val showAccount = accounts.toMutableList().apply { val item = getDefaultAccount(context) @@ -213,22 +344,25 @@ object DataStoreHelper { add(0, item) } - val builder = - BottomSheetDialog(context) - builder.setContentView(binding.root) val accountName = context.getString(R.string.account) - binding.profilesRecyclerview.setLinearListLayout( - isHorizontal = true, - nextUp = FOCUS_SELF, - nextDown = FOCUS_SELF, - nextLeft = FOCUS_SELF, - nextRight = FOCUS_SELF - ) + binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account, true) - builder.dismissSafe() + // Check if the selected account has a lock PIN set + if (account.lockPin != null) { + // Prompt for the lock pin + showPinInputDialog(context, account.lockPin, false) { pin -> + if (pin == null) return@showPinInputDialog + // Pin is correct, unlock the profile + setAccount(account, true) + builder.dismissSafe() + } + } else { + // No lock PIN set, directly set the account + setAccount(account, true) + builder.dismissSafe() + } }, addAccountCallback = { val currentAccounts = accounts @@ -264,7 +398,6 @@ object DataStoreHelper { builder.show() } - data class PosDur( @JsonProperty("position") val position: Long, @JsonProperty("duration") val duration: Long @@ -282,20 +415,35 @@ object DataStoreHelper { /** * Used to display notifications on new episodes and posters in library. **/ - data class SubscribedData( + abstract class LibrarySearchResponse( @JsonProperty("id") override var id: Int?, - @JsonProperty("subscribedTime") val bookmarkedTime: Long, - @JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, - @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, + @JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long, @JsonProperty("name") override val name: String, @JsonProperty("url") override val url: String, @JsonProperty("apiName") override val apiName: String, - @JsonProperty("type") override var type: TvType? = null, + @JsonProperty("type") override var type: TvType?, @JsonProperty("posterUrl") override var posterUrl: String?, - @JsonProperty("year") val year: Int?, - @JsonProperty("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - ) : SearchResponse { + @JsonProperty("year") open val year: Int?, + @JsonProperty("syncData") open val syncData: Map?, + @JsonProperty("quality") override var quality: SearchQuality?, + @JsonProperty("posterHeaders") override var posterHeaders: Map? + ) : SearchResponse + + data class SubscribedData( + @JsonProperty("subscribedTime") val subscribedTime: Long, + @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) { fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -311,18 +459,19 @@ object DataStoreHelper { } data class BookmarkedData( - @JsonProperty("id") override var id: Int?, @JsonProperty("bookmarkedTime") val bookmarkedTime: Long, - @JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, - @JsonProperty("name") override val name: String, - @JsonProperty("url") override val url: String, - @JsonProperty("apiName") override val apiName: String, - @JsonProperty("type") override var type: TvType? = null, - @JsonProperty("posterUrl") override var posterUrl: String?, - @JsonProperty("year") val year: Int?, - @JsonProperty("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - ) : SearchResponse { + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) { fun toLibraryItem(id: String): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( name, @@ -337,6 +486,34 @@ object DataStoreHelper { } } + data class FavoritesData( + @JsonProperty("favoritesTime") val favoritesTime: Long, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) { + fun toLibraryItem(): SyncAPI.LibraryItem? { + return SyncAPI.LibraryItem( + name, + url, + id?.toString() ?: return null, + null, + null, + null, + latestUpdatedTime, + apiName, type, posterUrl, posterHeaders, quality, this.id + ) + } + } + data class ResumeWatchingResult( @JsonProperty("name") override val name: String, @JsonProperty("url") override val url: String, @@ -369,15 +546,9 @@ object DataStoreHelper { removeKeys(folder) } - fun deleteAllBookmarkedData() { - val folder1 = "$currentAccount/$RESULT_WATCH_STATE" - val folder2 = "$currentAccount/$RESULT_WATCH_STATE_DATA" - removeKeys(folder1) - removeKeys(folder2) - } - fun deleteBookmarkedData(id: Int?) { if (id == null) return + AccountManager.localListApi.requireLibraryRefresh = true removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) } @@ -475,6 +646,12 @@ object DataStoreHelper { return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) } + fun getAllBookmarkedData(): List { + return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull { + getKey(it) + } ?: emptyList() + } + fun getAllSubscriptions(): List { return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull { getKey(it) @@ -510,6 +687,29 @@ object DataStoreHelper { return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) } + fun getAllFavorites(): List { + return getKeys("$currentAccount/$RESULT_FAVORITES_STATE_DATA")?.mapNotNull { + getKey(it) + } ?: emptyList() + } + + fun removeFavoritesData(id: Int?) { + if (id == null) return + AccountManager.localListApi.requireLibraryRefresh = true + removeKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) + } + + fun setFavoritesData(id: Int?, data: FavoritesData) { + if (id == null) return + setKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString(), data) + AccountManager.localListApi.requireLibraryRefresh = true + } + + fun getFavoritesData(id: Int?): FavoritesData? { + if (id == null) return null + return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) + } + fun setViewPos(id: Int?, pos: Long, dur: Long) { if (id == null) return if (dur < 30_000) return // too short 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/GlideApp.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/GlideApp.kt index 4b0ee890..8d73db3d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/GlideApp.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/GlideApp.kt @@ -8,6 +8,7 @@ import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.request.RequestOptions @@ -27,6 +28,10 @@ class GlideModule : AppGlideModule() { RequestOptions() .diskCacheStrategy(DiskCacheStrategy.ALL) .signature(ObjectKey(System.currentTimeMillis().toShort())) + }.setDiskCache { + // Possible to make this a setting in the future. + val memoryCacheSizeBytes: Long = 1024 * 1024 * 100; // 100mb + InternalCacheDiskCacheFactory(context, memoryCacheSizeBytes).build() } } 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/PercentageCropImageView.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PercentageCropImageView.kt new file mode 100644 index 00000000..1e572fb7 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PercentageCropImageView.kt @@ -0,0 +1,94 @@ +package com.lagradost.cloudstream3.utils +//Reference: https://stackoverflow.com/a/29055283 +import android.content.Context +import android.graphics.Matrix +import android.graphics.drawable.Drawable +import android.util.AttributeSet + +class PercentageCropImageView : androidx.appcompat.widget.AppCompatImageView { + private var mCropYCenterOffsetPct: Float? = null + private var mCropXCenterOffsetPct: Float? = null + constructor(context: Context?) : super(context!!) + constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) + constructor( + context: Context?, attrs: AttributeSet?, + defStyle: Int + ) : super(context!!, attrs, defStyle) + + var cropYCenterOffsetPct: Float + get() = mCropYCenterOffsetPct!! + set(cropYCenterOffsetPct) { + require(cropYCenterOffsetPct <= 1.0) { "Value too large: Must be <= 1.0" } + mCropYCenterOffsetPct = cropYCenterOffsetPct + } + var cropXCenterOffsetPct: Float + get() = mCropXCenterOffsetPct!! + set(cropXCenterOffsetPct) { + require(cropXCenterOffsetPct <= 1.0) { "Value too large: Must be <= 1.0" } + mCropXCenterOffsetPct = cropXCenterOffsetPct + } + + private fun myConfigureBounds() { + if (this.scaleType == ScaleType.MATRIX) { + + val d = this.drawable + if (d != null) { + val dWidth = d.intrinsicWidth + val dHeight = d.intrinsicHeight + val m = Matrix() + val vWidth = width - this.paddingLeft - this.paddingRight + val vHeight = height - this.paddingTop - this.paddingBottom + val scale: Float + var dx = 0f + var dy = 0f + if (dWidth * vHeight > vWidth * dHeight) { + val cropXCenterOffsetPct = + if (mCropXCenterOffsetPct != null) mCropXCenterOffsetPct!!.toFloat() else 0.5f + scale = vHeight.toFloat() / dHeight.toFloat() + dx = (vWidth - dWidth * scale) * cropXCenterOffsetPct + } else { + val cropYCenterOffsetPct = + if (mCropYCenterOffsetPct != null) mCropYCenterOffsetPct!!.toFloat() else 0f + scale = vWidth.toFloat() / dWidth.toFloat() + dy = (vHeight - dHeight * scale) * cropYCenterOffsetPct + } + m.setScale(scale, scale) + m.postTranslate((dx + 0.5f).toInt().toFloat(), (dy + 0.5f).toInt().toFloat()) + this.imageMatrix = m + } + } + } + + // These 3 methods call configureBounds in ImageView.java class, which + // adjusts the matrix in a call to center_crop (android's built-in + // scaling and centering crop method). We also want to trigger + // in the same place, but using our own matrix, which is then set + // directly at line 588 of ImageView.java and then copied over + // as the draw matrix at line 942 of ImageView.java + override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean { + val changed = super.setFrame(l, t, r, b) + myConfigureBounds() + return changed + } + + override fun setImageDrawable(d: Drawable?) { + super.setImageDrawable(d) + myConfigureBounds() + } + + override fun setImageResource(resId: Int) { + super.setImageResource(resId) + myConfigureBounds() + } + // In case you can change the ScaleType in code you have to call redraw() + //fullsizeImageView.setScaleType(ScaleType.FIT_CENTER); + //fullsizeImageView.redraw(); + fun redraw() { + val d = this.drawable + if (d != null) { + // Force toggle to recalculate our bounds + setImageDrawable(null) + setImageDrawable(d) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 8285b8ab..f34e7238 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -2,14 +2,12 @@ package com.lagradost.cloudstream3.utils import android.app.Activity import android.app.Dialog +import android.text.Spanned import android.view.LayoutInflater import android.view.View import android.widget.AbsListView import android.widget.ArrayAdapter -import android.widget.EditText -import android.widget.ImageView import android.widget.LinearLayout -import android.widget.ListView import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone @@ -19,7 +17,10 @@ import androidx.core.view.marginRight import androidx.core.view.marginTop import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding +import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding +import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes @@ -54,14 +55,14 @@ object SingleSelectionHelper { if (this == null) return if (isTvSettings()) { - val builder = - AlertDialog.Builder(this, R.style.AlertDialogCustom) - .setView(R.layout.options_popup_tv) + val binding = OptionsPopupTvBinding.inflate(layoutInflater) + val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom) + .setView(binding.root) + .create() - val dialog = builder.create() dialog.show() - dialog.findViewById(R.id.listview1)?.let { listView -> + binding.listview1.let { listView -> listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE listView.adapter = ArrayAdapter(this, R.layout.sort_bottom_single_choice_color).apply { @@ -74,7 +75,7 @@ object SingleSelectionHelper { } } - dialog.findViewById(R.id.imageView)?.apply { + binding.imageView.apply { isGone = poster.isNullOrEmpty() setImage(poster) } @@ -105,12 +106,12 @@ object SingleSelectionHelper { if (this == null) return val realShowApply = showApply || isMultiSelect - val listView = binding.listview1//.findViewById(R.id.listview1)!! - val textView = binding.text1//.findViewById(R.id.text1)!! - val applyButton = binding.applyBtt//.findViewById(R.id.apply_btt) - val cancelButton = binding.cancelBtt//findViewById(R.id.cancel_btt) + val listView = binding.listview1 + val textView = binding.text1 + val applyButton = binding.applyBtt + val cancelButton = binding.cancelBtt val applyHolder = - binding.applyBttHolder//.findViewById(R.id.apply_btt_holder) + binding.applyBttHolder applyHolder.isVisible = realShowApply if (!realShowApply) { @@ -173,8 +174,8 @@ object SingleSelectionHelper { } } - private fun Activity?.showInputDialog( + binding: BottomInputDialogBinding, dialog: Dialog, value: String, name: String, @@ -184,11 +185,11 @@ object SingleSelectionHelper { ) { if (this == null) return - val inputView = dialog.findViewById(R.id.nginx_text_input)!! - val textView = dialog.findViewById(R.id.text1)!! - val applyButton = dialog.findViewById(R.id.apply_btt)!! - val cancelButton = dialog.findViewById(R.id.cancel_btt)!! - val applyHolder = dialog.findViewById(R.id.apply_btt_holder)!! + val inputView = binding.nginxTextInput + val textView = binding.text1 + val applyButton = binding.applyBtt + val cancelButton = binding.cancelBtt + val applyHolder = binding.applyBttHolder applyHolder.isVisible = true textView.text = name @@ -350,11 +351,17 @@ object SingleSelectionHelper { dismissCallback: () -> Unit, callback: (String) -> Unit, ) { - val builder = BottomSheetDialog(this) // probably the stuff at the bottom - builder.setContentView(R.layout.bottom_input_dialog) // input layout + val builder = BottomSheetDialog(this) + + val binding: BottomInputDialogBinding = BottomInputDialogBinding.inflate( + LayoutInflater.from(this) + ) + + builder.setContentView(binding.root) builder.show() showInputDialog( + binding, builder, value, name, @@ -363,4 +370,24 @@ object SingleSelectionHelper { dismissCallback ) } + + fun Activity.showBottomDialogText( + title: String, + text: Spanned, + dismissCallback: () -> Unit + ) { + val binding = BottomTextDialogBinding.inflate(layoutInflater) + val dialog = BottomSheetDialog(this) + + dialog.setContentView(binding.root) + + binding.dialogTitle.text = title + binding.dialogText.text = text + + dialog.setOnDismissListener { + dismissCallback.invoke() + } + + dialog.show() + } } 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..d5357e0c 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 @@ -178,9 +178,10 @@ object UIHelper { fun Activity?.navigate(@IdRes navigation: Int, arguments: Bundle? = null) { try { if (this is FragmentActivity) { - (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.navController?.navigate( - navigation, arguments - ) + val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment? + navHostFragment?.navController?.let { + it.navigate(navigation, arguments) + } } } catch (t: Throwable) { logError(t) 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/account_list_item.xml b/app/src/main/res/layout/account_list_item.xml new file mode 100644 index 00000000..3331b85b --- /dev/null +++ b/app/src/main/res/layout/account_list_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account_select.xml b/app/src/main/res/layout/activity_account_select.xml new file mode 100644 index 00000000..9138f82d --- /dev/null +++ b/app/src/main/res/layout/activity_account_select.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account_select_tv.xml b/app/src/main/res/layout/activity_account_select_tv.xml new file mode 100644 index 00000000..87340ad2 --- /dev/null +++ b/app/src/main/res/layout/activity_account_select_tv.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_text_dialog.xml b/app/src/main/res/layout/bottom_text_dialog.xml new file mode 100644 index 00000000..01b4834d --- /dev/null +++ b/app/src/main/res/layout/bottom_text_dialog.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index 54df59a8..f164384b 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -9,7 +9,7 @@ android:layout_margin="5dp" android:foreground="@drawable/outline_drawable" app:cardBackgroundColor="@color/transparent" - + android:focusable="true" app:cardCornerRadius="@dimen/rounded_image_radius" app:cardElevation="0dp"> @@ -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_head.xml b/app/src/main/res/layout/fragment_home_head.xml index 90386ccf..0edaf230 100644 --- a/app/src/main/res/layout/fragment_home_head.xml +++ b/app/src/main/res/layout/fragment_home_head.xml @@ -92,15 +92,7 @@ android:layout_height="100dp" android:layout_gravity="bottom" android:gravity="center" - android:orientation="vertical"> - - + android:orientation="horizontal"> - + diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index f9ea6974..6db7536f 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -55,6 +55,7 @@ android:layout_gravity="end" android:background="@drawable/player_button_tv_attr_no_bg" android:contentDescription="@string/search" + android:focusable="true" android:nextFocusLeft="@id/home_preview_change_api" android:nextFocusRight="@id/home_preview_switch_account" android:nextFocusDown="@id/home_preview_info_btt" @@ -70,6 +71,7 @@ android:layout_gravity="end" android:background="@drawable/player_button_tv_attr_no_bg" android:contentDescription="@string/account" + android:focusable="true" android:nextFocusLeft="@id/home_preview_search_button" android:nextFocusRight="@id/home_preview_switch_account" android:nextFocusDown="@id/home_preview_info_btt" @@ -230,7 +232,9 @@ android:layout_marginStart="@dimen/navbar_width" android:layout_marginEnd="0dp" android:padding="12dp" - android:text="@string/continue_watching" /> + android:text="@string/continue_watching" + android:background="?android:attr/selectableItemBackground" + app:drawableTint="?attr/white" /> + + + + + + + @@ -160,8 +176,7 @@ android:layout_height="40dp" android:layout_gravity="bottom" android:background="?attr/primaryGrayBackground" - android:descendantFocusability="blocksDescendants" - android:focusable="false" + android:focusable="true" android:paddingHorizontal="5dp" app:layout_scrollFlags="noScroll" app:tabGravity="center" diff --git a/app/src/main/res/layout/fragment_library_tv.xml b/app/src/main/res/layout/fragment_library_tv.xml new file mode 100644 index 00000000..22b9feb1 --- /dev/null +++ b/app/src/main/res/layout/fragment_library_tv.xml @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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_swipe.xml b/app/src/main/res/layout/fragment_result_swipe.xml index 4e8e3c14..eb2653d0 100644 --- a/app/src/main/res/layout/fragment_result_swipe.xml +++ b/app/src/main/res/layout/fragment_result_swipe.xml @@ -74,7 +74,7 @@ android:nextFocusUp="@id/result_back" android:nextFocusDown="@id/result_description" android:nextFocusLeft="@id/result_add_sync" - android:nextFocusRight="@id/result_share" + android:nextFocusRight="@id/result_favorite" tools:visibility="visible" @@ -89,10 +89,27 @@ android:layout_gravity="end|center_vertical" app:tint="?attr/textColor" /> + + - - + android:scaleType="matrix" + tools:src="@drawable/profile_bg_dark_blue" > + - @@ -155,7 +154,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:layout_width="match_parent" android:layout_height="wrap_content"> - + android:orientation="vertical" + android:layout_marginTop="175dp"> + + + + + + + + + + + + + + - - - - - - - - - - - @@ -310,19 +314,41 @@ https://developer.android.com/design/ui/tv/samples/jet-fit style="@style/ResultButtonTV" android:nextFocusRight="@id/result_description" android:nextFocusUp="@id/result_play_trailer" - android:nextFocusDown="@id/result_episodes_show" + android:nextFocusDown="@id/result_favorite_button" android:text="@string/type_none" android:visibility="visible" app:icon="@drawable/ic_baseline_bookmark_24" /> + + + + diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 5fec8c6a..8b352c1f 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -50,6 +50,7 @@ app:queryBackground="@color/transparent" app:queryHint="@string/search_hint" app:searchIcon="@drawable/search_icon" + app:closeIcon="@drawable/ic_baseline_close_24" tools:ignore="RtlSymmetry"> @@ -84,7 +85,8 @@ android:nextFocusLeft="@id/main_search" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" - android:nextFocusDown="@id/search_autofit_results" + android:nextFocusDown="@id/tvtypes_chips_scroll" + android:tag = "@string/tv_no_focus_tag" android:src="@drawable/ic_baseline_tune_24" app:tint="?attr/textColor" /> @@ -141,6 +143,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/homepage_parent_emulator.xml b/app/src/main/res/layout/homepage_parent_emulator.xml new file mode 100644 index 00000000..b3e1f2da --- /dev/null +++ b/app/src/main/res/layout/homepage_parent_emulator.xml @@ -0,0 +1,35 @@ + + + + + + + \ No newline at end of file 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/lock_pin_dialog.xml b/app/src/main/res/layout/lock_pin_dialog.xml new file mode 100644 index 00000000..db2af48e --- /dev/null +++ b/app/src/main/res/layout/lock_pin_dialog.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file 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/quick_search.xml b/app/src/main/res/layout/quick_search.xml index eeec4a5c..12d94aaa 100644 --- a/app/src/main/res/layout/quick_search.xml +++ b/app/src/main/res/layout/quick_search.xml @@ -17,14 +17,15 @@ android:layout_height="wrap_content"> + android:id="@+id/quick_search_back" + android:layout_gravity="center" + android:foregroundGravity="center" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:src="@drawable/ic_baseline_arrow_back_24" + app:tint="@android:color/white" + android:focusable="true" + android:layout_width="25dp" + android:layout_height="wrap_content"> @@ -48,6 +49,8 @@ app:queryBackground="@color/transparent" app:searchIcon="@drawable/search_icon" + app:closeIcon="@drawable/ic_baseline_close_24" + android:paddingStart="-10dp" android:iconifiedByDefault="false" app:queryHint="@string/search_hint" 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/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 6d0a94fb..d0df339b 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -331,57 +331,56 @@ app:exitAnim="@anim/exit_anim" app:popEnterAnim="@anim/enter_anim" app:popExitAnim="@anim/exit_anim" - tools:layout="@layout/main_settings"> - - - - - - - - + tools:layout="@layout/main_settings" /> + + + + + + + - \ No newline at end of file + 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..3ea31b54 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -2,4 +2,110 @@ %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..f73081f7 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -36,7 +36,6 @@ مكتمل مهمل أخطط لمشاهدته - لا شيء إعادة المشاهدة مشاهدة الفيلم تشغيل بث حي @@ -74,7 +73,6 @@ ازالة إعداد حالة المشاهدة تطبيق - إلغاء نسخ إغلاق مسح @@ -99,13 +97,13 @@ إضغط بإستمرار لإعادة التعيين للإعدادات الافتراضية إستيراد خطوط بوضعها هنا %s متابعة المشاهدة - حذف - مزيد من المعلومات + ازالة + المزيد من المعلومات قد تكون هناك حاجة إلى VPN لكي يعمل هذا المزود بشكل صحيح - هذا المزود هو تورنت ، يوصى باستخدام شبكة ظاهرية خاصة + هذا المزود هو تورنت، يوصى باستخدام شبكة ظاهرية خاصة لا يتم توفير البيانات الوصفية بواسطة الموقع، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع. الوصف - لم يتم العثور على وصف + لم يتم العثور على حبكة لم يتم العثور على وصف عرض سجل الاخطاء 🐈 نافذة منبثقة @@ -180,6 +178,7 @@ لم يتم العثور على أي حلقات حذف الملف حذف + إلغاء إيقاف مؤقت إستئناف -٣٠ @@ -198,7 +197,7 @@ القصة في قائمة الانتظار الترجمة ليست موجودة - الإفتراضي + الإفتراضي فارغ مستخدم التطبيق @@ -581,8 +580,8 @@ تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s حدد الوضع لتصفية تنزيل المكونات الإضافية تعطيل - @string/default_subtitles لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) لقد صوتت بالفعل + معدل النسخ الإحتياطي diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index a1042b7e..11dbddc0 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -34,7 +34,6 @@ الأنواع توقف التنزيل خطط للمشاهدة - لا يوجد إعادة المشاهدة !تم العثور على تحديث جديد \n%s->%s @@ -125,6 +124,7 @@ موسم تم نسخ الرابط إلى الحافظة مسح + الغي وقف جارٍ تنزيل تحديث التطبيق… إعادة التعيين إلى القيمة العادية @@ -208,7 +208,6 @@ استئناف تحميل معلومات وقفة التحميل - الغي احفظ إعدادات الترجمة لون الخط diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index d76aa94b..e8c5d0f5 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -41,7 +41,6 @@ Завършено Изпуснат План за гледане - Нито един Повторно гледане Пускане на филм Възпроизвеждане на живо @@ -78,7 +77,6 @@ Премахване Задайте статус на гледане Приложи - Отказ Копирай Затвори Изчисти @@ -159,7 +157,7 @@ Скрий избраното видео качество в резултатите от търсенето Автоматични актуализации на плъгини Показвай актуализации на приложението - Автоматично търси нови актуализации при стартиране + Автоматично търси нови актуализации при стартиране на приложението. Актуализация до експериментални версии Търсете експериментални актуализации вместо само пълни версии Github @@ -187,6 +185,7 @@ Няма намерени епизоди Изтрий файла Изтрий + Отказ Пауза Продължи -30 @@ -205,7 +204,7 @@ Синопсис На опашката Без субтитри - По подразбиране + По подразбиране Безплатно Използвано Приложения @@ -223,8 +222,8 @@ Филм Серия Анимационен филм - @string/anime - @string/ova + Аниме + ОВА Торент Документален филм Азиатска драма @@ -260,7 +259,7 @@ Не показвай отново Пропуснете тази актуализация Актуализация - Предпочитано качество за гледане + Предпочитано качество за гледане (през WiFi) Максимален брой знаци за заглавие във видеоплейъра Разделителна способност на видео плейъра Размер на видео буфера @@ -505,4 +504,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..b0359c1c 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -43,7 +43,6 @@ শেষ বাদ দেখার ইচ্ছায় - কোন কিছুই না পুনরায় দেখা হচ্ছে টরেন্ট স্ট্রিম করুন উৎসসমূহ @@ -72,7 +71,6 @@ বাদ দিন বুকমার্ক করুন প্রয়োগ করুন - বাদ দিন কপি করুন বন্ধ করুন মুছুন @@ -127,9 +125,9 @@ বিরতি দিতে মাঝে চাপুন সিস্টেম এর উজ্জ্বলতা ব্যবহার করুন ট্রেইলার চালু করুন - ভিডিওপ্লেয়ার এ সময় নিয়ন্ত্রণ করতে, ডানে অথবা বামে সোয়াইপ করুন + ভিডিওর সময় নিয়ন্ত্রণ করতে, ডানে অথবা বামে সোয়াইপ করুন সেটিংস পরিবর্তন করতে সোয়াইপ করুন - উজ্জ্বলতা অথবা স্বরমাত্রা পরিবর্তন করতে যথাক্রমে বামে অথবা ডানে সোয়াইপ করুন + উজ্জ্বলতা বা ভলিউম পরিবর্তন করতে বাম বা ডান দিকে উপরে বা নিচে স্লাইড করুন স্বয়ংক্রিয়ভাবে পরবর্তী পর্ব চালান বর্তমান পর্বটি শেষ হলে পরের পর্বটি চালান থামতে দুইবার চাপুন @@ -143,10 +141,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..1c26c236 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -44,7 +44,6 @@ Completado Deixado Planejando assistir - Nenhum Reassistindo Assistir Filme Transmitir Torrent @@ -81,7 +80,6 @@ Remover Selecionar marcador Aplicar - Cancelar Copiar Fechar Limpar @@ -127,14 +125,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 +141,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 +158,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 @@ -185,6 +183,7 @@ Nenhum Episódio encontrado Apagar Arquivo Deletar + Cancelar Pausar Retomar -30 @@ -203,7 +202,7 @@ Sinopse Na fila Sem Legendas - Padrão + Padrão Livre Usado App @@ -223,7 +222,7 @@ Série Desenho Animado Anime - @string/ova + OVA Torrent Documentário Drama Asiático @@ -264,7 +263,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 +325,7 @@ /\?\? /%d %s autenticado - Falha em autenticar para %s + Não foi possível fazer login em %s Nenhum Normal @@ -340,8 +339,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..8da04be6 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 @@ -54,7 +54,6 @@ Dovršeno Ispušteno Planiram pogledati - Ništa Ponovno gledam Pokreni Film Pokreni LiveStream @@ -89,10 +88,9 @@ Informacije Filtriraj oznake Oznake - Makni + Ukloni Postavi status gledanja Primijeni - Poništi Kopiraj Zatvori Očisti @@ -117,7 +115,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 +128,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 +149,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 @@ -200,6 +198,7 @@ Nisu pronađene epizode Izbriši datoteku Izbriši + Poništi Pauziraj Nastavi -30 @@ -218,7 +217,7 @@ Sinopsis u redu čekanja Bez titlova - Zadano + Zadano Slobodno Iskorišteno Aplikacija @@ -306,7 +305,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 +313,7 @@ Primarna boja Tema aplikacije Mjesto naslova postera - Stavi naslov ispod postera + Stavlja naslov ispod postera lozinka123 MojeCoolIme @@ -398,7 +397,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 +445,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 +471,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 +494,7 @@ Geste Značajke playera Titlovi - Respored + Raspored Zadane postavke Izgled Značajke @@ -527,7 +526,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. @@ -567,7 +566,6 @@ Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka Onemogući - @string/default_subtitles U repozitoriju nisu pronađeni dodaci Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. @@ -577,4 +575,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-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 677beaf8..4cd322a9 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -20,6 +20,7 @@ Letöltés Keresés Törlés + Mégse Szüneteltetés sorba állítva Igazítás @@ -61,7 +62,6 @@ Nézés Befejezve Később megnézés - Nincs Újranézés Film lejátszása Előzetes lejátszása @@ -90,7 +90,6 @@ Eltávolítás Megtekintés állapotának beállítása Alkalmazás - Mégse Másolás Bezárás Törlés @@ -162,7 +161,7 @@ Értékelés Rajzfilmek Élőadások - Alapértelmezett + Alapértelmezett Filmek TV sorozat Anime diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index d514bcc4..a6c5813d 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -39,7 +39,6 @@ Selesai Dihentikan Rencana untuk Menonton - Tidak Ada Menonton Ulang Putar Movie Streaming Torrent @@ -75,7 +74,6 @@ Hapus Atur status tontonan Terapkan - Batalkan Salin Tutup Bersihkan @@ -174,6 +172,7 @@ Episode Tidak Ditemukan Hapus File Hapus + Batalkan Jeda Lanjutkan -30 @@ -192,7 +191,7 @@ Sinopsis antri Tidak Ada Subtitle - Default + Default Tersedia Terpakai Aplikasi @@ -575,5 +574,4 @@ Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN Kamu sudah voting - @string/default_subtitles diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0c34e89a..933ac77f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -44,7 +44,6 @@ Completato Abbandonato Da guardare - Nessuno Riguardando Riproduci film Riproduci Livestream @@ -81,7 +80,6 @@ Rimuovi Imposta stato riproduzione Applica - Cancella Copia Chiudi Cancella @@ -190,6 +188,7 @@ Nessun episodio trovato Elimina file Elimina + Cancella Pausa Riprendi -30 @@ -208,7 +207,7 @@ Sinossi In coda Nessun sottotiolo - Default + Default Libero Usato App @@ -572,7 +571,6 @@ Repository non trovato, controlla l\'URL e prova la VPN Non è stato possibile creare correttamente l\'interfaccia utente, questo è un GRANDE BUG e dovrebbe essere segnalato immediatamente %s Seleziona la modalità per filtrare il download dei plugin - @string/default_subtitles Disabilita Hai già votato diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index aaa65897..55666df5 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -66,7 +66,6 @@ הסר הגדר מצב צפייה ליישם - בטל העתק לסגור נקה @@ -78,7 +77,6 @@ צופה כתוביות בהמתנה - ללא להוריד מדובב יותר מידע @@ -132,7 +130,7 @@ העדכון התחיל מידע התקן אוטומטית את כל התוספים שטרם הותקנו ממאגרים שנוספו. - חפש אוטומטית עדכונים חדשים בפתיחת האפליקציה + חפש אוטומטית עדכונים חדשים לאחר פתיחת האפליקציה. הצג עדכונים לאפליקציה בצע מחדש את תהליך ההגדרה עדכן למהדורות מוקדמות @@ -146,7 +144,8 @@ לא נמצאו פרקים מחק קובץ מחק - עצור + בטל + השהה המשך -30 +30 @@ -159,7 +158,7 @@ דירוג שנה ללא כתוביות - ברירת מחדל + ברירת מחדל חינם משומש סדרת טלוויזיה @@ -176,7 +175,7 @@ אל תראה שוב דלג על עדכון זה עדכון - איכות צפייה מועדפת + איכות צפייה מועדפת (WiFi) נגן וידאו כותרת מקסימום תווים רזולוציית נגן וידאו הוסף מעקב @@ -184,7 +183,7 @@ הבא ספריה מטא-דאטה לא מסופק על ידי האתר, טעינת הסרטון תיכשל אם הוא לא קיים באתר. - החלק על הצד השמאלי או הימני כדי לשנות את הבהירות או עוצמת הקול + החלק בצד השמאלי או הימני כדי לשנות את הבהירות או עוצמת הקול שחזור הנתונים מהקובץ נכשל %s עדכונים וגיבויים נותן לך את תוצאות החיפוש מופרדות לפי ספק @@ -194,7 +193,7 @@ כמות בנינים שניתנו עונה מצטערים, האפליקציה קרסה. דוח באג אנונימי יישלח למפתחים - טורנט + טורנטים NSFW שגיאת מעבד הורד מראה @@ -226,8 +225,8 @@ תכונות נגן עדכן התקדמות צפייה DNS מעל HTTPS - לחץ באמצע כדי לעצור - החלק שמאלה או ימינה כדי לשלוט על זמן בנגן הסרטונים + לחץ פעמיים באמצע כדי לעצור + החלק מצד לצד כדי לשלוט על מיקומך בסרטון נגן אוטומטית את הפרק הבא התחל את הפרק הבא כאשר הפרק הנוכחי נגמר לחץ פעמיים על צד ימין או שמאל כדי להציץ קדימה או אחורה @@ -264,11 +263,11 @@ איכות מצלמה החלק כדי להציץ גיבוי - כמות הצצת הנגן + כמות הזזת הנגן (שניות) סרטון גיטהאב מצלמה - החלק לשינוי ההגדרות + החלק כדי לשנות הגדרות ‪דפדפן צבע חלון הצג לוג @@ -438,7 +437,7 @@ מיין בחר ספרייה נראה שהספרייה שלכם ריקה :( -\nהתחברו לחשבון ספריה או הוסף סדרות לספרייה המקומית שלך +\nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם. קובץ מצב בטוח נמצא! \nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. לא ניתן להתקין את הגרסה החדשה של האפליקציה @@ -505,5 +504,51 @@ אלפביתי (א \'עד ת\') אלפביתי (ת\' עד א\') פתח עם - נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת + נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת. + רשום ל%s + מעדכן סדרות שנרשמת אליהן + נגן מוסתר - כמות הזזה + רשת סלולארית + רשום + חזור + הרשמה ל-%s מבוטלת + פרוקסי עבור raw.githubusercontent.com + פרופילים + הממשק לא נוצר נכון. זוהי שגיאה רצינית, נא לדווח עליה מיידית ל-%s + עריכה + Wi-Fi + רקע הפרופיל + רשומה + עזרה + התחלה + עצור + פרופיל %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..16341b60 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -37,7 +37,6 @@ アジアドラマ ライブ配信 NSFW - キャンセル アニメ ロック ソース @@ -71,7 +70,6 @@ ソース 履歴 ポスター - なし コピー 閉じる 保存 @@ -135,6 +133,7 @@ 一時停止 再生エピソード 削除 + キャンセル 開始 状態 @@ -155,7 +154,7 @@ バージョン 視聴率 %s 視聴率 - デフォルト + デフォルト ダウンロード失敗 ダウンロード開始 ダウンロード完了 @@ -218,4 +217,5 @@ フォントサイズ プロバイダーから探す 言語の自動選択 + リンクの読み込みエラー diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 399aafb1..07ff89e4 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -32,7 +32,6 @@ ಮಾಹಿತಿ ಸೆಟ್ ವಾಚ್ ಸ್ಟೇಟಸ್ ಅನ್ವಯಿಸು - ರದ್ದುಮಾಡು ಸಬ್ ಟೈಟಲ್ಸ್ ಎಲೆವಷನ್ ಫಾಂಟ್ ಸೈಜ್ ಸಬ್ ಟೈಟಲ್ಸ್ ಭಾಷೆ @@ -108,7 +107,6 @@ ಪ್ರಕಾರಗಳು ಬ್ರೌಸರ್ ತೆರೆಯಿರಿ ಆನ್-ಹೋಲ್ಡ್ - ನನ್ ಸಂಪರ್ಕವನ್ನು ಮರುಪ್ರಯತ್ನಿಸಿ… ಡೌನ್‌ಲೋಡ್ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ ಡೌನ್‌ಲೋಡ್ ವಿಫಲವಾಗಿದೆ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 74c05d07..ef2f6cc5 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -35,7 +35,6 @@ 시청 완료 포기 시청 예정 - 없음 다시보기 영화 재생 예고편 재생 @@ -95,7 +94,6 @@ 백업 더빙 자막 - 취소 북마크 필터 북마크 제거 @@ -177,7 +175,7 @@ 개요 대기중 자막 없음 - 기본 + 기본 남음 사용됨 @@ -352,6 +350,7 @@ 아시아 드라마 시즌 삭제 + 취소 %s %d%s 파일 삭제 일시정지 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..db5ac011 --- /dev/null +++ b/app/src/main/res/values-lt/strings.xml @@ -0,0 +1,264 @@ + + + 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 + Atšaukti + 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 + 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ą + 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-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index ddd39942..14e5b600 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -31,7 +31,6 @@ Pabeigts Atmests Plāno skatīties - Neviena Atkārtoti skatities Palaist Filmu Palaist Trelleri @@ -67,7 +66,6 @@ Noņemt Ieliec skatīšanās statusu Izmantot - Atcelt Kopēt Saglabāt Atskaņošanas ātrums @@ -192,6 +190,7 @@ Epizodes netika atrastas Dzēsti faili Dzēst + Atcelt Pauzēt Sākt Neizdevās @@ -483,7 +482,7 @@ Iet Bezmaksas Ieslēgt elementus uz plakātiem - Parastais + Parastais Nav atjauninājumi atrasti Izdzēst video un bildes atkritne Izvēlētā skatīšanās kvalitāte (Mobilie Dati) diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 66a6b9ba..7badfd18 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -25,7 +25,6 @@ Завршени Отфрлени Планирани за гледање - Ништо одбрано Повторно гледање Пушти филм Стримај торент @@ -58,7 +57,6 @@ Обележувачи Отстрани Активирај - Откажи Брзина на плеер Поставки за преводи Боја на текстот @@ -133,6 +131,7 @@ Е Избриши датотека Избриши + Откажи Паузирај Продолжи Ова трајно ќе го избрише %s @@ -147,7 +146,7 @@ Крат во редица Нема преводи - Стандардно + Стандардно Слободен простор Искористен простор Апликациски простор diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 3d6240f9..cdc50cea 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -54,7 +54,6 @@ ബുക്മാർക് നീക്കം ചെയ്യുക പ്രയോഗിക്കുക - റദ്ദാക്കുക പ്ലേയർ വേഗത + Default --> ഒഴിവ് ഉപയോഗത്തിൽ ആപ്പ് @@ -197,7 +197,6 @@ ദാതാവിനെ മാറ്റുക ലോഡിംഗ്… ബ്രൗസർ - ഒന്നുമില്ല വീണ്ടും കാണുക സ്ട്രീം diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 17eeb883..e579381e 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -37,4 +37,22 @@ Kelajuan (%.2fx) Poster Poster + Salin + Muat Turun + Genre + Cari %s… + Sunting + Carian… + Pelayar + Episod seterusnya + Muat Turun + Buka dengan + Padam Fail + Buka Dalam Pelayar + Tiada Data + Info + Simpan + Kongsi + Tetapan + Tutup diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 0cb44373..e0346f19 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -32,7 +32,6 @@ ကြည့်နေသည် ကြည့်ပြီး ကြည့်ခြင်းရပ်ထားသော - ဘာမျှ လင့်များချိတ်ဆက်ရာတွင်အချို့အယွင်း ဖုန်း သိုလှောင်ရုံ စာမှတ်များ စစ်ထုတ်မှု @@ -127,8 +126,7 @@ အကျဥ်းချုပ် နောက်အစီအစဥ် စာတန်းထိုးမထည့် - ပုံသေ - @string/default_subtitles + ပုံသေ ကျန်ရှိသော အက်ပ် ရုပ်ရှင်များ @@ -253,7 +251,6 @@ စာတန်းထိုး ဘာသာစကား ဒီမှာနေရာချခြင်းဖြင့်ဖောင့်များကိုသွင်းပါ %s အတည်ပြု - ပယ်ဖျက်ရန် စာတန်းထိုး ပြုပြင်ခြင်း စာသား အရောင် အနားကွပ် အရောင် @@ -301,6 +298,7 @@ အပိုင်းများမတွေ့ပါ ဖိုင်ကိုဖျက်ရန် ဖျက်ရန် + ပယ်ဖျက်ရန် ရပ်ရန် စရန် မအောင်မြင်ပါ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d19726fd..d73d77a0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -44,7 +44,6 @@ Voltooid Dropped Plan om te kijken - Geen Opnieuw kijken Film afspelen Livestream afspelen @@ -82,7 +81,6 @@ Verwijder Zet kijkstatus Toepassen - annuleer Kopiëren Sluit Wissen @@ -186,6 +184,7 @@ Geen afleveringen gevonden Verwijder bestand Verwijder + annuleer Pauze Hervatten -30 @@ -204,7 +203,7 @@ Korte inhoud wachtrij Geen ondertiteling - Standaard + Standaard Vrij Gebruikt App @@ -573,6 +572,5 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - @string/default_subtitles Je hebt al gestemd diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 592ff22c..c70ebd4b 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -63,7 +63,6 @@ Fjern Sett visingstatus Bruk - Avbryt Kopier Tøm Lagre @@ -113,6 +112,7 @@ Ingen episodar blei funnen Slett fil Slett + Avbryt Pause Gjenoppta -30 @@ -129,7 +129,7 @@ Om i kø Ingen undertekstar - Standard + Standard Brukt Program Filmar @@ -166,7 +166,6 @@ Gå tilbake Sjangrar Dele - Ingen Ser om igjen Oppdatering starta Fortsett nedlasting diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index dac15d61..9845120b 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -33,7 +33,6 @@ Fullført Falt Planlegg å se - Ingen Spill filmer Strøm Torrent Kilder @@ -67,7 +66,6 @@ Bokmerker Ta bort Søke om - Avbryt Spillerhastighet Innstillinger for teksting Tekstfarge @@ -141,6 +139,7 @@ E Slett fil Slett + Avbryt Stopp Gjenoppta Dette vil slette %s @@ -155,7 +154,7 @@ Om I kø Ingen undertekster - Misligholde + Misligholde Tilgjengelig Brukt applikasjon diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 177f7ea1..22f7d1ca 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -19,7 +19,6 @@ ଵେଗ (%.2fଗୁଣ) ତ୍ୟାଗିଛନ୍ତି ଦେଖିବା ପାଇଁ ଇଚ୍ଛୁକ - କିଛି ନାହିଁ ଅଧିକ ସୂଚନା ପାତ୍ର: %s ପୋଷ୍ଟର୍ @@ -42,7 +41,7 @@ ଅଧ୍ୟାୟର ପୋଷ୍ଟର୍ ମୁଖ୍ୟ ପୋଷ୍ଟର୍ - ଡିଫଲ୍ଟ + ଡିଫଲ୍ଟ ଭାଷା ନାହିଁ ଵର୍ଣ୍ଣନା @@ -158,4 +157,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-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a170d610..95a3e1c6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -35,7 +35,6 @@ Zakończone Porzucone Planowane - Brak Ponowne oglądanie Odtwórz film Odtwórz transmisję na żywo @@ -72,7 +71,6 @@ Usuń Ustaw status oglądania Zastosuj - Anuluj Kopiuj Zamknij Wyczyść @@ -181,6 +179,7 @@ Nie znaleziono odcinków Usuń plik Usuń + Anuluj Wstrzymaj Odtwórz -30 @@ -199,7 +198,7 @@ Streszczenie W kolejce Brak napisów - Domyślne + Domyślne Wolne W użyciu Aplikacja @@ -552,7 +551,6 @@ Nie można było poprawnie utworzyć interfejsu użytkownika, jest to POWAŻNY BŁĄD i należy go natychmiast zgłosić %s Wybierz tryb filtrowania pobieranych rozszerzeń Wyłączać - @string/default_subtitles Nie znaleziono żadnych wtyczek w repozytorium Już oddano głos Nie znaleziono tego repozytorium, sprawdź adres URL lub spróbuj połączyć się przez VPN diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 908ddb0d..c73fb996 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -42,7 +42,6 @@ Concluído Desistido Pretendo assistir - Nenhum Reassistindo Reproduzir filme Reproduzir transmissão ao vivo @@ -78,7 +77,6 @@ Marcadores Remover Aplicar - Cancelar Copiar Fechar Limpar @@ -182,6 +180,7 @@ Nenhum Episódio encontrado Eliminar Ficheiro Eliminar + Cancelar Pôr em Pausa Retomar Isto apagará %s permanentemente @@ -198,7 +197,7 @@ Sinopse Na fila Sem Legendas - Padrão + Padrão Livre Usado App @@ -548,7 +547,6 @@ \nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! Selecionar o modo para filtrar a transferência de plug-ins Não foi possível criar corretamente a interface do utilizador, trata-se de um GRANDE BUG e deve ser comunicado imediatamente %s - \@ string/legendas_padrão Desativar Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 9c68c008..e1191d2b 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -23,7 +23,6 @@ ahhahooo ooooo haa aaahhuoh - aaaghh aaaaa aaaghh aaaghhaaahhu aaaaa oha aauuh @@ -51,7 +50,6 @@ oouuh ooh aauuh ooh oouuhaooo-ahah - oooohh oouuh aauuh oha ouuhhhooooooh ooo-ahah ohaouuhhh @@ -122,6 +120,7 @@ A ooha ohahaaahoooa ahahooo oooooah + oooohh aahhaaaaaahooo aauuh aaahhuaooo-ahahoooohh aoouuhoohoohooo-ahah %s aaaghhaaaaa aauuhaauuh @@ -133,7 +132,7 @@ aauugghh ohaaauugghh haaouuhhh ahaaaaaaaaaahaaaauugghh - ahooooh + ahooooh ooo-ahahaaaaa ahhahhh aauugghh diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index b6971c37..75388b23 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -43,7 +43,6 @@ Finalizat Renunțat Planificare pentru a urmări - Anuleaza Reurmat Urmărește Stream Torrent @@ -79,7 +78,6 @@ Eliminează Adaugă Aplică - Anulează Copiază Închide Elimină @@ -181,6 +179,7 @@ Nu s-au găsit episoade Ștergeți fișierul Ștergeți + Anulează Pauză Continuă -30 @@ -199,7 +198,7 @@ Rezumat În coada de așteptare Nu există subtitrare - Implicit + Implicit Liber Folosit Aplicație @@ -569,6 +568,5 @@ Utilizați UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor - @string/default_subtitles Ați votat deja diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bcd3fc0f..64c3146e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -12,6 +12,7 @@ Скачать неудачный Подогнать Удалить + Отмена Все Пауза Актёрский состав: %s @@ -58,7 +59,6 @@ Завершено Брошенный План посмотреть - Нет Пересмотрю Смотреть фильм Смотреть трейлер @@ -82,7 +82,6 @@ Фильтр закладки Закладки Применить - Отмена Копия Закрыть Очистить @@ -90,7 +89,7 @@ Скорость проигрыватель Воспроизвести Эпизод %dд %dч %dм - %d мин. + %d мин Dub Sub Установите смотреть состояние @@ -186,7 +185,7 @@ Рейтинг Продолжительность Нет субтитров - По умолчанию + По умолчанию Приложение Аниме Торренты @@ -200,7 +199,7 @@ Автоматически загружать еще не установленные плагины из добавленных репозиториев. Присоединится в Discord Бесплатно - %d мин. + %dm %d ч. %d мин. Фильмы Мультфильм @@ -529,4 +528,27 @@ Обход ограничения доступа к GitHub с помощью jsDelivr может задержать обновления на несколько дней. Подписные Отказались от подписки на %s + Мобильный интернет + Профили + Пользовательский интерфейс не был доступен для правильного создания, это ГЛАВНАЯ ОШИБКА и должна быть сообщена немедленно %s + Изменить + Интернет + Задний фон профиля + Помощь + Профиль %d + Выберите режим фильтера плагинов для загрузки + Качество + Использовать + Выключить + Хранилище не обнаружено, проверьте URL и попробуйте с ВПН + Вы уже проголосовали + Никаких плагинов не обнаружено + Поставить обычный + Здесь вы можете изменить порядок расположения источников. Если видео имеет более высокий приоритет, оно будет отображаться выше в списке источников. Сумма приоритета источника и приоритета качества составляет приоритет видео. +\n +\nИсточник А: 3 +\nКачество Б: 7 +\nБудет иметь общий приоритет видео 10. +\n +\nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index e0cc27d0..4f17971e 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -33,7 +33,6 @@ Hľadať… Sťahovanie Žiadne dáta - Zrušiť Kopírovať Zavrieť Uložiť @@ -60,7 +59,6 @@ Sťahovanie zrušené Dab Zmazať súbor - Žiadny Tit Opätovné sledovanie Prehrať súbor @@ -265,7 +263,7 @@ Preferované médiá URL servera NGINX %d %s - Predvolené + Predvolené Pridať sledovanie Žiadna sezóna Epizóda @@ -293,6 +291,7 @@ Rok Prispôsobiť obrazovke Zmazať + Zrušiť Využité Štítok kvality Prehrávač skrytý - dĺžka pretočenia diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index db82d9fa..c3d107f5 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -10,6 +10,7 @@ Dejintii ma guulaysan Raadi Tirtir + Jooji Haki Horran Le-ekaysii shaashadda @@ -46,7 +47,6 @@ Dhamaystirmay Soo dhacay Ku talo jira - Midna Daaro filinka Daar goos-gooska Midabka daaqadda @@ -56,7 +56,6 @@ Daalaco toorentiga Qrl-hoosaadka Dib u bilow dejinta - Jooji Midabka dambeedka Xigashooyinka Dib u xidhiidhinaya… @@ -202,7 +201,7 @@ Muddada La isticmaalay Qrl-hoosaad ma leh - Sidiisaa + Sidiisaa Bilaash Appka Kartoob diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 397faa48..806a1ca3 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -26,7 +26,6 @@ Avslutad Dropped Plannerad - Ingen Spela Upp Strömma Torrent Källor @@ -53,7 +52,6 @@ Bokmärken Ta bort Tillämpa - Avbryt Spelarhastighet Undertextinställningar Textfärg @@ -125,6 +123,7 @@ A Ta bort nerladdad fil Ta bort + Avbryt %s kommer att raderas permanent \nÄr du helt säker\? Pågående @@ -137,7 +136,7 @@ Sammanfattning på kö Inga undertexter - Standard + Standard Tillgängligt Använtt App diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 3f4134e5..ca4a1377 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) முகப்பு தேடு பதிவிறக்கம் @@ -38,7 +38,6 @@ மேலும் தகவல்கள் மறை நீக்கு - ரத்து செய்க நீக்கு சேமிக்கவும் உரை வண்ணம் @@ -60,10 +59,9 @@ தேடுவதற்கு இருமுறை தட்டவும் பிளேயரில் தேடுதல் வேகம் இடைநிறுத்துவதற்கு நடுவில் தட்டவும் - நடிகர்கள் + நடிகர்கள்: %s பின் செல் அமைப்புகள் - ஏதும் இல்லை ஏற்றுகிறது… கைவிடப்பட்டது பதிவிறக்கம் முடிந்தது @@ -117,4 +115,9 @@ புதிய புதுப்பிப்பு உள்ளது \n%s->%s நிரப்பி + போஸ்டர் + எபிசோட்டின் போஸ்டர் + போஸ்டர் + பிரதான போஸ்டர் + %s Ep %d diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 95d38478..03138259 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -36,7 +36,6 @@ Tapos nang panoorin Ayaw nang panoorin Balak panoorin - None I-play ang Movie Stream Torrent Sources @@ -70,7 +69,6 @@ Bookmark Tanggalin Kumpirmahin - Kanselahin Bilis ng Playback Subtitle Setting Kulay ng Teksto @@ -144,6 +142,7 @@ E Burahin ang file Tanggalin + Kanselahin I-pause I-resume This will permanently delete %s @@ -158,7 +157,7 @@ Sinopsis nakapila walang subtitles - Default + Default Bakante Gamit App 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..c1e06a7b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -57,7 +57,6 @@ Tamamlandı Bırakıldı Planlandı - Hiçbiri Yeniden izleniyor Filmi oynat Canlı yayını oynat @@ -96,7 +95,6 @@ Kaldır İzleme durumunu ayarla Uygula - İptal et Kopyala Kapat Temizle @@ -205,7 +203,7 @@ Bölüm bulunamadı Dosyayı sil Sil - @string/sort_cancel + İptal et Durdur Sürdür -30 @@ -224,7 +222,7 @@ Özet Sıraya alındı Alt yazı yok - Varsayılan + Varsayılan Boş Kullanılan Uygulama @@ -594,4 +592,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..57b128de 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -38,7 +38,7 @@ Переглянути фільм Переглянути трейлер Трансляція через торент - Повторити підключення… + Повторити з\'єднання… Назад Переглянути епізод Завантажено @@ -112,9 +112,7 @@ Переглянути файл Детальніше Фільтр закладок - Нічого - Скасувати - Очистити + Очистити Налаштування субтитрів Колір фону Висота субтитрів @@ -136,7 +134,7 @@ Крок перемотки (секунди) Натисніть двічі посередині, щоб призупинити відтворення відео Використовувати яскравість системи - Оновити прогрес перегляду + Оновлювати прогрес перегляду Відновлення даних з резервної копії Резервне копіювання даних Не вдалося відновити дані з файлу %s @@ -174,6 +172,7 @@ Е Видалити файл Видалити + Скасувати Відновити -30 Це назавжди видалить %s @@ -186,14 +185,14 @@ Тривалість у черзі Без субтитрів - За замовчуванням + За замовчуванням Вільно Зайнято Застосунок Телесеріали Мультфільми Аніме - ОВА + OVA Азіатські драми Прямі трансляції Інше @@ -206,7 +205,7 @@ Відео Помилка джерела Віддалена помилка - Помилка рендеринга + Помилка рендерингу Дзеркало Chromecast Переглянути в застосунку Переглянути в %s @@ -214,7 +213,7 @@ Завантажити дзеркало Перевірити наявність оновлень Заблокувати - Пропускати OP + Пропустити ОП Не показувати знову Оновити Бажана якість перегляду (WiFi) @@ -233,7 +232,7 @@ Автоматично шукає нові оновлення після запуску застосунку. Оновлення до бета-версій Посилання скопійовано в буфер обміну - Деякі телефони не підтримують новий інсталятор пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. + Деякі телефони не підтримують новий встановлювач пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. Приєднуйтесь до Discord Дано бананів Рік @@ -549,8 +548,8 @@ Не вдалося створити UI коректно, це ВАЖЛИВА ПОМИЛКА, про яку слід негайно повідомити %s Виберіть режим для фільтрації завантаження плагінів Вимкнути - @string/default_subtitles Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували + Частота резервного копіювання diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 48b73efa..b437e2d0 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -28,13 +28,12 @@ مزید آپشنز اگلا قسط براؤزر میں کھولیں - لوڈ کرنا سکیپ کر دیں + لوڈنگ چھوڑیں لوڈ ہو رہا ہے… - دیکھنا - آن ہولڈ - گرا دیا - دیکھنا ہے - کوئی نہیں + دیکھ رہے ہیں + معطل + چھوڑ دیا گیا + دیکھنے کا منصوبہ دوبارہ دیکھنا مووی لگائے ٹریلر چلائیں @@ -67,7 +66,6 @@ ریمو واچ اسٹیٹس کو سیٹ کریں لاگو کریں - منسوخ کریں کاپی بند کریں صاف کریں @@ -95,7 +93,7 @@ کھلا (آن) یہ سورس Torrent ہے، ضرورت پڑنے پر VPN کا استعمال کریں تفصیل - کوئی تفصیل نہیں ملی + کوئی کہانی نہیں ملی کوئی تفصیل نہیں ملی غلطی کا لاگ دیکھیں 🐈 PIP @@ -134,14 +132,14 @@ ایپ کی تازہ کاریاں نمایش کریں سیٹ اپ کا عمل دوبارہ کریں پری ریلیز کے لیے اپ ڈیٹ کریں - صرف مکمل ریلیز کے بجائے پری ریلیز اپ ڈیٹس تلاش کریں + صرف مکمل جاریات کی بجائے پیش رلیز اپ ڈیٹس کی تلاش کریں APK انسٹالر Github - ایک ہی dev کی طرف سے light ناول اپلی کیشن - اسی devs کے ذریعے anime ایپ + یہ لائٹ ناول ایپ وہی ڈویلپرز نے تیار کی ہے جو اس ایپ کو ڈویلپ کیا ہے + یہ اینمی ایپ وہی ڈویلپرز نے تیار کی ہے جو اس ایپ کو ڈویلپ کیا ہے ڈسکارڈ میں شامل ہوں - دیووں کو ایک بینین دیں - دی گئی بینین + شکریہ ڈویلپرز کو! آپ کا کام شاندار ہے + تعریفیں ایپ کی زبان اس فراہم کنندہ کے پاس کروم کاسٹ سپورٹ نہیں ہے کوئی لنکس نہیں ملے @@ -150,7 +148,7 @@ طے شدہ قدر پر ری سیٹ کریں %s قسط %d پوسٹر - شیئر + شئیر کریں انواع رفتار (%.2fx) مکمل @@ -178,7 +176,7 @@ anime کے لیے فلر ایپیسوڈ دکھائیں پلگ ان خود بخود ڈاؤن لوڈ کریں شامل کردہ ذخیروں سے خود بخود تمام ابھی تک انسٹال نہیں ہوئے پلگ ان انسٹال کریں۔ - شروع ہونے پر خودکار طور پر نئی اپ ڈیٹس تلاش کریں + اپلیکیشن کو شروع کرنے کے بعد خود بخود نئی اپ ڈیٹس کی تلاش کریں۔ کچھ فون نئے پیکیج انسٹالر کو سپورٹ نہیں کرتے ہیں. اگر اپ ڈیٹس انسٹال نہیں ہوتے ہیں تو لیگیسی آپشن کو آزمائیں. معذرت، ایپلی کیشن کریش ہو گئی. ایک گمنام بگ رپورٹ ڈویلپرز کو بھیجی جائے گی سیزن @@ -193,6 +191,7 @@ کوئی اقساط نہیں ملی فائل کو ڈیلیٹ کریں مٹا دیں + منسوخ کریں توقف از سر نو شروع کریں -30 @@ -211,7 +210,7 @@ خلاصہ قطار میں کوئی سب ٹائٹلز نہیں - ڈیفالٹ + ڈیفالٹ مفت استعمال شُدہ کارٹون @@ -233,7 +232,7 @@ اووا ٹورینٹ دستاویزی فلم - ایشیائی ڈرامے + ایشین ڈرامہ لائیو اسٹریمز ویڈیو ماخذ نقص @@ -333,15 +332,15 @@ سایہ ذیلی ہم وقت سازی کریں 1000 ms - سب ٹائٹل تاخیر + زیرنویس میں دیری اگر سب ٹائٹلز %d ms بہت جلد دکھائے جائیں تو اسے استعمال کریں - سب ٹائٹل تاخیر + کوئی زیرنویس میں دیری نہیں این ایس ایف ڈبلیو آٹو ڈاؤن لوڈ براؤزر میں چلائیں کم میموری والے آلات، جیسے کہ Android TV پر بہت زیادہ سیٹ ہونے پر کریشوں کا سبب بنتا ہے. سائٹ ہٹائیں - ایک مختلف URL کے ساتھ ، موجودہ سائٹ کا کلون شامل کریں. + ایک مختلف URL کے ساتھ ، موجودہ سائٹ کا کلون شامل کریں اسکرین پر فٹ کھینچیں اکاؤنٹ بنائیں @@ -488,7 +487,7 @@ غلط ID مخزن کا نام ختم ہونے والا - کھل رہا + کھولنا کیا آپ کو یقین ہے کہ آپ ہیاں سے نکلنا چاہتے ہیں؟ ایس ڈی تمام زبانیں @@ -519,12 +518,37 @@ سب ٹائٹلز سے بند کیپشنز کو ہٹا دیں اپنے آلے کے مطابق ایپ کی شکل تبدیل کریں اگلے - CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ -\nSky UK Limited 🤮 کی طرف سے بے دماغ DMCA ہٹانے کی وجہ سے ہم ایپ میں ریپوزٹری سائٹ کو لنک نہیں کر سکتے۔ -\nہمارے ڈسکارڈ میں شامل ہوں یا آن لائن تلاش کریں۔ + CloudStream کے ساتھ پہلے سے کوئی ویب سائٹس انسٹال نہیں ہیں۔ آپ کو مخزنوں سے ویب سائٹس انسٹال کرنی ہوں گی۔ +\n +\nSky UK Limited کی طرف سے DMCA کے تعلیم نامے کی بنا پر 🤮، ہم ایپ کے اندر مخزن سائٹ کی مستقیم لنک فراہم نہیں کر سکتے۔ +\n +\nبراہ کرم ہمارے ڈسکارڈ کمیونٹی میں شامل ہونے کو مد نظر رکھیں یا مخزن کیلئے آن لائن تلاش کریں۔ تمام ایکسٹینشنز کریش کی وجہ سے آف کر دی گئیں تاکہ آپ کو پریشانی کا باعث تلاش کرنے میں مدد مل سکے۔ پہلے ایکسٹینشن انسٹال کریں بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ ایپ اپ ڈیٹ انسٹال ہو رہا ہے… یہ فہرست خالی ہے۔ کسی اور پر سوئچ کرنے کی کوشش کریں۔ + موبائل ڈیٹا + پروفائلز + یو آئی درست طریقے سے تخلیق نہیں کی جاسکتی تھی، یہ ایک بڑا بگ ہے اور فوراً رپورٹ کیا جانا چاہئے%s + ترتیب دیں + وائی فائی + پروفائل پس منظر + مدد + پروفائل %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-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 217d2791..8186d6ed 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -45,7 +45,6 @@ Đã xem Bỏ qua Xem sau - Mặc định Xem lại Xem Ngay Phát trực tiếp @@ -83,7 +82,6 @@ Xóa Đặt trạng thái xem Áp dụng - Hủy bỏ Sao lưu Đóng Huỷ bỏ @@ -190,6 +188,7 @@ Không có tập nào Xóa Tệp Xóa + Hủy bỏ Tạm Dừng Tiếp Tục -30 @@ -208,7 +207,7 @@ Thông tin Hàng chờ Không có phụ đề - Mặc Định + Mặc Định Còn trống Đã sử dụng App @@ -567,5 +566,4 @@ Không tìm thấy plugin Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s Chọn chế độ để lọc plugin tải xuống - @string/default_subtitles diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1fd01d8a..52897633 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -57,7 +57,6 @@ 觀看完畢 放棄觀看 計畫觀看 - 重新觀看 播放電影 播放直播 @@ -96,7 +95,6 @@ 移除 設定觀看狀態 套用 - 取消 複製 關閉 清除 @@ -205,7 +203,7 @@ 未找到劇集 刪除文件 刪除 - @string/sort_cancel + 取消 暫停 繼續 -30 @@ -224,7 +222,7 @@ 簡介 已加入佇列 無字幕 - 預設 + 預設 空閒 已使用 應用程式 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 033a5f50..682bcab7 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -57,7 +57,6 @@ 观看完毕 放弃观看 计划观看 - 重新观看 播放电影 播放直播 @@ -96,7 +95,6 @@ 移除 设置观看状态 应用 - 取消 复制 关闭 清除 @@ -206,7 +204,7 @@ 未找到剧集 删除文件 删除 - @string/sort_cancel + 取消 暂停 继续 -30 @@ -225,7 +223,7 @@ 简介 已加入队列 无字幕 - 默认 + 默认 空闲 已使用 应用 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 84cc85c5..ff03d1fb 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 @@ -76,11 +77,11 @@ %d %.1f/10.0 %d - %s Ep %d + %1$s Ep %2$d Cast: %s Episode %d will be released in - %dd %dh %dm - %dh %dm + %1$dd %2$dh %3$dm + %1$dh %2$dm %dm Poster @@ -96,7 +97,7 @@ Speed (%.2fx) Rated: %.1f - New update found!\n%s -> %s + New update found!\n%1$s -> %2$s Filler %d min CloudStream @@ -122,7 +123,7 @@ Completed Dropped Plan to Watch - None + @string/none Rewatching Play Movie Play Trailer @@ -163,7 +164,7 @@ Remove Set watch status Apply - Cancel + @string/cancel Copy Close Clear @@ -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 @@ -274,18 +276,18 @@ developers Season - %s %d%s + %1$s %2$d%3$s No Season Episode Episodes - %d-%d - %d %s + %1$d-%2$d + %1$d %2$s S E No Episodes found Delete File Delete - @string/sort_cancel + Cancel Pause Start Failed @@ -305,8 +307,9 @@ Synopsis queued No Subtitles - Default - @string/default_subtitles + Default + @string/action_default + @string/action_default Free Used App @@ -475,7 +478,7 @@ Kitsu Trakt --> - %s %s + %1$s %2$s account Log out Log in @@ -576,8 +579,8 @@ Plugin Deleted Could not load %s 18+ - Started downloading %d %s… - Downloaded %d %s + Started downloading %1$d %2$s… + Downloaded %1$d %2$s All %s already downloaded No plugins found in repository Repository not found, check the URL and try VPN @@ -671,6 +674,8 @@ Subscribed to %s Unsubscribed from %s Episode %d released! + Subscribe + Unsubscribe Profile %d Wi-Fi Mobile data @@ -699,5 +704,41 @@ Info Sync data Syncing data + You have already voted + Favorites + %s added to favorites + %s removed from favorites + Add to favorites + Remove from favorites + + Potential Duplicate Found + Add + Replace + Replace All + @string/sort_cancel + + It appears that a potentially duplicate item already exists in your library: \'%1$s.\' + + \n\nWould you like to add this item anyway, replace the existing one, or cancel the action? + + + Potential duplicate items have been found in your library: + + \n\n%s + + \n\nWould you like to add this item anyway, replace the existing ones, or cancel the action? + + + + tv_no_focus_tag + + + Enter PIN + Enter Current PIN + Lock Profile + PIN + Incorrect PIN. Please try again. + PIN must be 4 characters + Select an Account 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" /> - - + + - - + -