From 540b17094f291c68193b4f0f766d51da014d6000 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 8 Mar 2024 02:07:14 +0100 Subject: [PATCH 001/157] =?UTF-8?q?Revert=20"feat:=20make=20cloudstream=20?= =?UTF-8?q?compilation=20and=20builds=20fast!=20using=20gradle=20conf?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 21b341e12fea168b828cd433b96bb5cf3a8fd46f. --- app/build.gradle.kts | 13 ++++--------- gradle.properties | 3 +-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e29ee12d..31e225de 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,13 +62,8 @@ android { versionCode = 63 versionName = "4.3.1" - // retrieve latest commit hash - val gitVersion = providers.exec { - commandLine("git", "rev-parse", "--short", "HEAD") - }.standardOutput.asText.get() - resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") - resValue("string", "commit_hash", gitVersion) + resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") resValue("bool", "is_prerelease", "false") // Reads local.properties @@ -168,10 +163,10 @@ dependencies { // Android Core & Lifecycle implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") - implementation("androidx.navigation:navigation-ui-ktx:2.7.7") + implementation("androidx.navigation:navigation-ui-ktx:2.7.6") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") // Design & UI implementation("jp.wasabeef:glide-transformations:4.3.0") @@ -187,7 +182,7 @@ dependencies { // For KSP -> Official Annotation Processors are Not Yet Supported for KSP ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:33.0.0-android") + implementation("com.google.guava:guava:32.1.3-android") implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") // Media 3 (ExoPlayer) diff --git a/gradle.properties b/gradle.properties index 8260a224..6a873a6a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,5 +20,4 @@ android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.nonTransitiveRClass=false -android.nonFinalResIds=false -org.gradle.configuration-cache=true \ No newline at end of file +android.nonFinalResIds=false \ No newline at end of file From e3f9f255c79c1d6cba31bd804a8aa25269004de4 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 8 Mar 2024 02:07:35 +0100 Subject: [PATCH 002/157] =?UTF-8?q?Revert=20"feat:=20make=20cloudstream=20?= =?UTF-8?q?compilation=20and=20builds=20fast!=20using=20gradle=20conf?= =?UTF-8?q?=E2=80=A6"=20(#968)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 21b341e12fea168b828cd433b96bb5cf3a8fd46f. --- app/build.gradle.kts | 13 ++++--------- gradle.properties | 3 +-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e29ee12d..31e225de 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,13 +62,8 @@ android { versionCode = 63 versionName = "4.3.1" - // retrieve latest commit hash - val gitVersion = providers.exec { - commandLine("git", "rev-parse", "--short", "HEAD") - }.standardOutput.asText.get() - resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") - resValue("string", "commit_hash", gitVersion) + resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") resValue("bool", "is_prerelease", "false") // Reads local.properties @@ -168,10 +163,10 @@ dependencies { // Android Core & Lifecycle implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") - implementation("androidx.navigation:navigation-ui-ktx:2.7.7") + implementation("androidx.navigation:navigation-ui-ktx:2.7.6") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") // Design & UI implementation("jp.wasabeef:glide-transformations:4.3.0") @@ -187,7 +182,7 @@ dependencies { // For KSP -> Official Annotation Processors are Not Yet Supported for KSP ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:33.0.0-android") + implementation("com.google.guava:guava:32.1.3-android") implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") // Media 3 (ExoPlayer) diff --git a/gradle.properties b/gradle.properties index 8260a224..6a873a6a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,5 +20,4 @@ android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.nonTransitiveRClass=false -android.nonFinalResIds=false -org.gradle.configuration-cache=true \ No newline at end of file +android.nonFinalResIds=false \ No newline at end of file From 694e7abbdfca247c6865834b90d058be55bec9f3 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 8 Mar 2024 03:00:00 +0100 Subject: [PATCH 003/157] Fixes #816 --- .../cloudstream3/mvvm/ArchComponentExt.kt | 4 ++++ .../ui/account/AccountSelectActivity.kt | 19 ++++++--------- .../ui/library/LibraryFragment.kt | 23 +++++++++++-------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index eb575775..817d7db3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -49,11 +49,15 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } +/** NOTE: Only one observer at a time per value */ fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) liveData.observe(this) { it?.let { t -> action(t) } } } +/** NOTE: Only one observer at a time per value */ fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) liveData.observe(this) { action(it) } } 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 index c6e1e1fe..ccaa38f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -68,6 +68,13 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet } } + observe(viewModel.isAllowedLogin) { isAllowedLogin -> + if (isAllowedLogin) { + // We are allowed to continue to MainActivity + navigateToMainActivity() + } + } + // Don't show account selection if there is only // one account that exists if (!isEditingFromMainActivity && skipStartup) { @@ -75,12 +82,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet if (currentAccount?.lockPin != null) { CommonActivity.init(this) viewModel.handleAccountSelect(currentAccount, this, true) - observe(viewModel.isAllowedLogin) { isAllowedLogin -> - if (isAllowedLogin) { - // We are allowed to continue to MainActivity - navigateToMainActivity() - } - } } else { if (accounts.count() > 1) { showToast(this, getString( @@ -108,12 +109,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet // Handle the selected account accountSelectCallback = { viewModel.handleAccountSelect(it, this) - observe(viewModel.isAllowedLogin) { isAllowedLogin -> - if (isAllowedLogin) { - // We are allowed to continue to MainActivity - navigateToMainActivity() - } - } }, accountCreateCallback = { viewModel.handleAccountUpdate(it, this) }, accountEditCallback = { 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 8e9c8521..fa91d990 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 @@ -131,6 +131,18 @@ class LibraryFragment : Fragment() { super.onSaveInstanceState(outState) } + private fun updateRandom() { + val position = libraryViewModel.currentPage.value ?: 0 + val pages = (libraryViewModel.pages.value as? Resource.Success)?.value ?: return + if (toggleRandomButton) { + listLibraryItems.clear() + listLibraryItems.addAll(pages[position].items) + binding?.libraryRandom?.isVisible = listLibraryItems.isNotEmpty() + } else { + binding?.libraryRandom?.isGone = true + } + } + @SuppressLint("ResourceType", "CutPasteId") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -395,15 +407,7 @@ class LibraryFragment : Fragment() { binding?.viewpager?.setCurrentItem(page, false) } - observe(libraryViewModel.currentPage){ - if (toggleRandomButton) { - listLibraryItems.clear() - listLibraryItems.addAll(pages[it].items) - libraryRandom.isVisible = listLibraryItems.isNotEmpty() - } else { - libraryRandom.isGone = true - } - } + updateRandom() // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating // Without this there would be a flashing effect: @@ -481,6 +485,7 @@ class LibraryFragment : Fragment() { } observe(libraryViewModel.currentPage) { position -> + updateRandom() val all = binding?.viewpager?.allViews?.toList() ?.filterIsInstance() From bd69054f5d5fad817f15442e85c5873de50ac8e6 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 8 Mar 2024 03:16:36 +0100 Subject: [PATCH 004/157] Updated lib icon --- app/src/main/res/drawable/library_icon.xml | 10 ++++++++++ app/src/main/res/menu/bottom_nav_menu.xml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/library_icon.xml diff --git a/app/src/main/res/drawable/library_icon.xml b/app/src/main/res/drawable/library_icon.xml new file mode 100644 index 00000000..f62dceac --- /dev/null +++ b/app/src/main/res/drawable/library_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index cb620bb8..830b004a 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -10,7 +10,7 @@ android:title="@string/title_search" /> Date: Sat, 9 Mar 2024 16:24:38 +0200 Subject: [PATCH 005/157] New TV UI (#950) --- .../lagradost/cloudstream3/MainActivity.kt | 20 +- .../cloudstream3/ui/home/HomeFragment.kt | 1 + .../ui/result/ResultFragmentTv.kt | 289 ++++--- .../ui/result/ResultViewModel2.kt | 7 +- .../res/drawable/ic_baseline_film_roll_24.xml | 10 + .../res/drawable/ic_baseline_resume_arrow.xml | 11 + .../drawable/ic_baseline_resume_arrow2.xml | 12 + .../res/drawable/outline_bookmark_add_24.xml | 5 + .../res/drawable/player_button_tv_attr.xml | 4 +- .../drawable/player_button_tv_attr_no_bg.xml | 2 +- app/src/main/res/layout/cast_item.xml | 1 - .../main/res/layout/fragment_home_head_tv.xml | 2 + app/src/main/res/layout/fragment_result.xml | 10 +- .../main/res/layout/fragment_result_tv.xml | 786 ++++++++---------- .../res/layout/player_custom_layout_tv.xml | 8 +- app/src/main/res/values/strings.xml | 5 +- app/src/main/res/values/styles.xml | 29 + 17 files changed, 633 insertions(+), 569 deletions(-) create mode 100644 app/src/main/res/drawable/ic_baseline_film_roll_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_resume_arrow.xml create mode 100644 app/src/main/res/drawable/ic_baseline_resume_arrow2.xml create mode 100644 app/src/main/res/drawable/outline_bookmark_add_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 6308117b..fa6cae18 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1189,8 +1189,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu newLocalBinding.focusOutline.isVisible = false } - if (isTrueTvSettings()) { + if(isTrueTvSettings()) { + // Put here any button you don't want focusing it to center the view + val exceptionButtons = listOf( + R.id.home_preview_play_btt, + R.id.home_preview_info_btt, + R.id.home_preview_hidden_next_focus, + R.id.home_preview_hidden_prev_focus, + R.id.result_play_movie_button, + R.id.result_play_series_button, + R.id.result_resume_series_button, + R.id.result_play_trailer_button, + R.id.result_bookmark_Button, + R.id.result_favorite_Button, + R.id.result_subscribe_Button, + R.id.result_search_Button, + R.id.result_episodes_show_button, + ) + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener centerView(newFocus) } } 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 d54ea488..cd843517 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 @@ -529,6 +529,7 @@ class HomeFragment : Fragment() { super.onScrolled(recyclerView, dx, dy) } }) + } 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 427e9cb3..85e948c2 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 @@ -33,13 +33,13 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer +import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData 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 import com.lagradost.cloudstream3.utils.AppUtils.loadCache @@ -129,9 +129,9 @@ class ResultFragmentTv : Fragment() { * Note that this will steal any focus if the episode loading is too slow (unlikely). */ private fun focusPlayButton() { - binding?.resultPlayMovie?.requestFocus() - binding?.resultPlaySeries?.requestFocus() - binding?.resultResumeSeries?.requestFocus() + binding?.resultPlayMovieButton?.requestFocus() + binding?.resultPlaySeriesButton?.requestFocus() + binding?.resultResumeSeriesButton?.requestFocus() } private fun setRecommendations(rec: List?, validApiName: String?) { @@ -246,37 +246,15 @@ class ResultFragmentTv : Fragment() { storedData.start ) // ===== ===== ===== + var comingSoon = false 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 - toggleEpisodes(false) - } - val rightListener: View.OnFocusChangeListener = - View.OnFocusChangeListener { _, hasFocus -> - if (!hasFocus) return@OnFocusChangeListener - toggleEpisodes(true) - } - - resultPlayMovie.onFocusChangeListener = leftListener - resultPlaySeries.onFocusChangeListener = leftListener - resultResumeSeries.onFocusChangeListener = leftListener - resultPlayTrailer.onFocusChangeListener = leftListener - 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 - toggleEpisodes(!episodeHolderTv.isVisible) - } - - // resultEpisodes.onFocusChangeListener = leftListener + // parallax on background + resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { view, _, scrollY, _, oldScrollY -> + backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f + }) redirectToPlay.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) return@setOnFocusChangeListener @@ -284,13 +262,14 @@ class ResultFragmentTv : Fragment() { binding?.apply { val views = listOf( - resultPlayMovie, - resultPlaySeries, - resultResumeSeries, - resultPlayTrailer, + resultPlayMovieButton, + resultPlaySeriesButton, + resultResumeSeriesButton, + resultPlayTrailerButton, resultBookmarkButton, resultFavoriteButton, - resultSubscribeButton + resultSubscribeButton, + resultSearchButton ) for (requestView in views) { if (!requestView.isVisible) continue @@ -299,11 +278,6 @@ class ResultFragmentTv : Fragment() { } } - // parallax on background - resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f - }) - redirectToEpisodes.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) return@setOnFocusChangeListener toggleEpisodes(true) @@ -313,7 +287,7 @@ class ResultFragmentTv : Fragment() { resultSeasonSelection, resultRangeSelection, resultEpisodes, - resultPlayTrailer, + resultPlayTrailerButton, ) for (requestView in views) { if (!requestView.isShown) continue @@ -322,6 +296,45 @@ class ResultFragmentTv : Fragment() { } } + mapOf( + resultPlayMovieButton to resultPlayMovieText, + resultPlaySeriesButton to resultPlaySeriesText, + resultResumeSeriesButton to resultResumeSeriesText, + resultPlayTrailerButton to resultPlayTrailerText, + resultBookmarkButton to resultBookmarkText, + resultFavoriteButton to resultFavoriteText, + resultSubscribeButton to resultSubscribeText, + resultSearchButton to resultSearchText, + resultEpisodesShowButton to resultEpisodesShowText + ).forEach { (button , text) -> + + button.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + text.isSelected = false + return@setOnFocusChangeListener + } + + text.isSelected = true + if (button.tag == context?.getString(R.string.tv_no_focus_tag)){ + resultFinishLoading.scrollTo(0,0) + } + when (button.id) { + R.id.result_episodes_show_button -> { + toggleEpisodes(true) + } + else -> { + toggleEpisodes(false) + } + } + } + } + + resultEpisodesShowButton.setOnClickListener { + // toggle, to make it more touch accessible just in case someone thinks that a + // tv layout is better but is using a touch device + toggleEpisodes(!episodeHolderTv.isVisible) + } + resultEpisodes.setLinearListLayout( isHorizontal = false, nextUp = FOCUS_SELF, @@ -430,9 +443,9 @@ class ResultFragmentTv : Fragment() { val aboveCast = listOf( binding?.resultEpisodesShow, - binding?.resultBookmarkButton, - binding?.resultFavoriteButton, - binding?.resultSubscribeButton, + binding?.resultBookmark, + binding?.resultFavorite, + binding?.resultSubscribe, ).firstOrNull { it?.isVisible == true } @@ -443,8 +456,15 @@ class ResultFragmentTv : Fragment() { observeNullable(viewModel.resumeWatching) { resume -> binding?.apply { + + // > resultResumeSeries is visible when not null + if (resume == null) { + resultResumeSeries.isVisible = false + return@observeNullable + } + // show progress no matter if series or movie - resume?.progress?.let { progress -> + resume.progress?.let { progress -> resultResumeSeriesProgressText.setText(progress.progressLeft) resultResumeSeriesProgress.apply { isVisible = true @@ -456,37 +476,24 @@ class ResultFragmentTv : Fragment() { resultResumeProgressHolder.isVisible = false } - // if movie then hide both as movie button is - // always visible on movies, this is done in movie observe - - if (resume?.isMovie == true) { - resultPlaySeries.isVisible = false - resultResumeSeries.isVisible = false - return@observeNullable - } - - // if series then - // > resultPlaySeries is visible when null - // > resultResumeSeries is visible when not null - if (resume == null) { - resultPlaySeries.isVisible = true - resultResumeSeries.isVisible = false - return@observeNullable - } - + resultPlayMovie.isVisible = false resultPlaySeries.isVisible = false resultResumeSeries.isVisible = true focusPlayButton() + // Stops last button right focus if it is a movie + if (resume.isMovie) + resultSearchButton.nextFocusRightId = R.id.result_search_Button - resultResumeSeries.text = - if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull( - null, // resume.result.name, we don't want episode title - resume.result.episode, - resume.result.season - ) + resultResumeSeriesText.text = + when { + resume.isMovie -> context?.getString(R.string.resume) + resume.result.season != null -> + "${getString(R.string.season_short)}${resume.result.season}:${getString(R.string.episode_short)}${resume.result.episode}" + else -> "${getString(R.string.episode)}${resume.result.episode}" + } - resultResumeSeries.setOnClickListener { + resultResumeSeriesButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent( storedData.playerAction, //?: ACTION_PLAY_EPISODE_IN_PLAYER, @@ -495,7 +502,7 @@ class ResultFragmentTv : Fragment() { ) } - resultResumeSeries.setOnLongClickListener { + resultResumeSeriesButton.setOnLongClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_SHOW_OPTIONS, resume.result) ) @@ -509,9 +516,9 @@ class ResultFragmentTv : Fragment() { context?.updateHasTrailers() if (!LoadResponse.isTrailersEnabled) return@observe val trailers = trailersLinks.flatMap { it.mirros } - binding?.resultPlayTrailer?.apply { - isGone = trailers.isEmpty() - setOnClickListener { + binding?.apply { + resultPlayTrailer.isGone = trailers.isEmpty() + resultPlayTrailerButton.setOnClickListener { if (trailers.isEmpty()) return@setOnClickListener activity.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( @@ -526,24 +533,38 @@ class ResultFragmentTv : Fragment() { } observe(viewModel.watchStatus) { watchType -> - binding?.resultBookmarkButton?.apply { - setText(watchType.stringRes) - setOnClickListener { view -> - activity?.showBottomDialog( - WatchType.values().map { view.context.getString(it.stringRes) }.toList(), - watchType.ordinal, - view.context.getString(R.string.action_add_to_bookmarks), - showApply = false, - {}) { - viewModel.updateWatchStatus(WatchType.values()[it], context) + binding?.apply { + resultBookmarkText.setText(watchType.stringRes) + + resultBookmarkButton.apply { + + val drawable = if (watchType.stringRes == R.string.type_none) { + R.drawable.outline_bookmark_add_24 + } else { + R.drawable.ic_baseline_bookmark_24 + } + setIconResource(drawable) + + setOnClickListener { view -> + activity?.showBottomDialog( + WatchType.entries.map { view.context.getString(it.stringRes) }.toList(), + watchType.ordinal, + view.context.getString(R.string.action_add_to_bookmarks), + showApply = false, + {}) { + viewModel.updateWatchStatus(WatchType.entries[it], context) + } } } } } observeNullable(viewModel.favoriteStatus) { isFavorite -> + + binding?.resultFavorite?.isVisible = isFavorite != null + binding?.resultFavoriteButton?.apply { - isVisible = isFavorite != null + if (isFavorite == null) return@observeNullable val drawable = if (isFavorite) { @@ -552,14 +573,8 @@ class ResultFragmentTv : Fragment() { 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 @@ -576,11 +591,21 @@ class ResultFragmentTv : Fragment() { } } } + + binding?.resultFavoriteText?.apply { + val text = if (isFavorite == true) { + R.string.unfavorite + } else { + R.string.favorite + } + setText(text) + } } observeNullable(viewModel.subscribeStatus) { isSubscribed -> + binding?.resultSubscribe?.isVisible = isSubscribed != null && requireContext().isEmulatorSettings() binding?.resultSubscribeButton?.apply { - isVisible = isSubscribed != null && context.isEmulatorSettings() + if (isSubscribed == null) return@observeNullable val drawable = if (isSubscribed) { @@ -589,14 +614,8 @@ class ResultFragmentTv : Fragment() { 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 @@ -614,32 +633,47 @@ class ResultFragmentTv : Fragment() { CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } } + + binding?.resultSubscribeText?.apply { + val text = if (isSubscribed) { + R.string.action_unsubscribe + } else { + R.string.action_subscribe + } + setText(text) + } } } observeNullable(viewModel.movie) { data -> + if (data == null) return@observeNullable + binding?.apply { - resultPlayMovie.isVisible = data is Resource.Success - resultPlaySeries.isVisible = data == null - seriesHolder.isVisible = data == null - resultEpisodesShow.isVisible = data == null + resultPlayMovie.isVisible = (data is Resource.Success) && !comingSoon + resultPlaySeries.isVisible = false + resultEpisodesShow.isVisible = false (data as? Resource.Success)?.value?.let { (text, ep) -> - resultPlayMovie.setText(text) - resultPlayMovie.setOnClickListener { + //resultPlayMovieText.setText(text) + resultPlayMovieButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) ) } - resultPlayMovie.setOnLongClickListener { + resultPlayMovieButton.setOnLongClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep) ) return@setOnLongClickListener true } - focusPlayButton() + //focusPlayButton() + resultPlayMovieButton.requestFocus() + + // Stops last button right focus + resultSearchButton.nextFocusRightId = R.id.result_search_Button } } + //focusPlayButton() } observeNullable(viewModel.selectPopup) { popup -> @@ -736,19 +770,26 @@ class ResultFragmentTv : Fragment() { // Used to request focus the first time the episodes are loaded. var hasLoadedEpisodesOnce = false observeNullable(viewModel.episodes) { episodes -> + if (episodes == null) return@observeNullable + binding?.apply { - resultEpisodes.isVisible = episodes is Resource.Success + + resultPlayMovie.isVisible = false + resultPlaySeries.isVisible = true && !comingSoon + resultEpisodes.isVisible = true && !comingSoon + resultEpisodesShow.isVisible = true && !comingSoon + // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { val first = episodes.value.firstOrNull() if (first != null) { - resultPlaySeries.text = context?.getNameFull( - null, // resume.result.name, we don't want episode title - first.episode, - first.season - ) - - resultPlaySeries.setOnClickListener { + resultPlaySeriesText.text = //"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" + when { + first.season != null -> + "${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" + else -> "${getString(R.string.episode)} ${first.episode}" + } + resultPlaySeriesButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent( ACTION_CLICK_DEFAULT, @@ -756,7 +797,7 @@ class ResultFragmentTv : Fragment() { ) ) } - resultPlaySeries.setOnLongClickListener { + resultPlaySeriesButton.setOnLongClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_SHOW_OPTIONS, first) ) @@ -765,6 +806,7 @@ class ResultFragmentTv : Fragment() { if (!hasLoadedEpisodesOnce) { hasLoadedEpisodesOnce = true focusPlayButton() + resultPlaySeries.requestFocus() } } @@ -826,6 +868,7 @@ class ResultFragmentTv : Fragment() { resultMetaYear.setText(d.yearText) resultMetaDuration.setText(d.durationText) resultMetaRating.setText(d.ratingText) + resultMetaStatus.setText(d.onGoingText) resultMetaContentRating.setText(d.contentRatingText) resultCastText.setText(d.actorsText) resultNextAiring.setText(d.nextAiringEpisode) @@ -859,8 +902,12 @@ class ResultFragmentTv : Fragment() { radius = 0, errorImageDrawable = error ) - resultComingSoon.isVisible = d.comingSoon + comingSoon = d.comingSoon + resultTvComingSoon.isVisible = d.comingSoon + resultPlayMovie.isGone = d.comingSoon + resultPlaySeries.isGone = d.comingSoon resultDataHolder.isGone = d.comingSoon + UIHelper.populateChips(resultTag, d.tags) resultCastItems.isGone = d.actors.isNullOrEmpty() (resultCastItems.adapter as? ActorAdaptor)?.updateList( @@ -871,6 +918,10 @@ class ResultFragmentTv : Fragment() { // If there is no rating to display, we don't want an empty gap resultMetaContentRating.width = 0 } + + resultSearchButton.setOnClickListener { + QuickSearchFragment.pushSearch(activity, d.title) + } } is Resource.Loading -> { 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 c24efe56..a05b4059 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 @@ -20,6 +20,7 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.getCastSession @@ -31,6 +32,7 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.syncproviders.providers.SimklApi @@ -261,8 +263,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { metaText = if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null, durationText = if (dur == null || dur <= 0) null else txt( - R.string.duration_format, - dur + secondsToReadable(dur * 60, "0 mins") ), onGoingText = if (this is EpisodeResponse) { txt( @@ -2464,7 +2465,7 @@ class ResultViewModel2 : ViewModel() { ResumeProgress( progress = (viewPos.position / 1000).toInt(), maxProgress = (viewPos.duration / 1000).toInt(), - txt(R.string.resume_time_left, (viewPos.duration - viewPos.position) / (60_000)) + txt(R.string.resume_remaining, secondsToReadable(((viewPos.duration - viewPos.position) / 1_000).toInt(), "0 mins")) ) } diff --git a/app/src/main/res/drawable/ic_baseline_film_roll_24.xml b/app/src/main/res/drawable/ic_baseline_film_roll_24.xml new file mode 100644 index 00000000..941d936f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_film_roll_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_resume_arrow.xml b/app/src/main/res/drawable/ic_baseline_resume_arrow.xml new file mode 100644 index 00000000..0326fbd4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_resume_arrow.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_resume_arrow2.xml b/app/src/main/res/drawable/ic_baseline_resume_arrow2.xml new file mode 100644 index 00000000..fc533a0e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_resume_arrow2.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/outline_bookmark_add_24.xml b/app/src/main/res/drawable/outline_bookmark_add_24.xml new file mode 100644 index 00000000..a4e18af3 --- /dev/null +++ b/app/src/main/res/drawable/outline_bookmark_add_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/player_button_tv_attr.xml b/app/src/main/res/drawable/player_button_tv_attr.xml index 4c90a64e..ed83887d 100644 --- a/app/src/main/res/drawable/player_button_tv_attr.xml +++ b/app/src/main/res/drawable/player_button_tv_attr.xml @@ -3,13 +3,13 @@ - + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml index b9b927da..0dd8c256 100644 --- a/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml +++ b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index f164384b..99a9750b 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -17,7 +17,6 @@ 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_result.xml b/app/src/main/res/layout/fragment_result.xml index 70461518..045b9cc2 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -412,7 +412,7 @@ android:foreground="@drawable/outline_drawable" android:maxLength="1000" android:nextFocusUp="@id/result_back" - android:nextFocusDown="@id/result_bookmark_button" + android:nextFocusDown="@id/result_bookmark_Button" android:paddingTop="5dp" android:textColor="?attr/textColor" android:textSize="15sp" @@ -474,7 +474,7 @@ android:fadingEdge="horizontal" android:focusable="false" android:focusableInTouchMode="false" - android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusUp="@id/result_bookmark_Button" android:nextFocusDown="@id/result_play_movie" android:orientation="horizontal" android:paddingTop="5dp" @@ -580,7 +580,7 @@ android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:layout_marginBottom="10dp" - android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusUp="@id/result_bookmark_Button" android:nextFocusDown="@id/result_download_movie" android:text="@string/play_movie_button" android:visibility="visible" @@ -658,7 +658,7 @@ android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:layout_marginBottom="10dp" - android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusUp="@id/result_bookmark_Button" android:nextFocusDown="@id/result_download_movie" android:text="@string/resume" android:visibility="visible" @@ -674,7 +674,7 @@ android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:layout_marginBottom="10dp" - android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusUp="@id/result_bookmark_Button" android:nextFocusDown="@id/result_download_movie" android:text="@string/next_episode" android:visibility="gone" diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index a7ba4334..ba8b728e 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -78,6 +78,30 @@ https://developer.android.com/design/ui/tv/samples/jet-fit + + + + + + + + + - - - - - - - - - - + android:layout_marginTop="225dp"> @@ -221,157 +220,289 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:textStyle="normal" tools:text="5d 3h 30m" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginTop="10dp" + android:baselineAligned="false"> + app:layout_constraintTop_toTopOf="parent" + tools:ignore="UselessParent"> - - - - - - - - - - - - - @@ -513,10 +594,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:descendantFocusability="afterDescendants" android:fadingEdge="horizontal" - android:focusable="false" - android:focusableInTouchMode="false" - android:nextFocusUp="@id/result_episodes_show" - android:nextFocusDown="@id/result_recommendations_filter_selection" + android:nextFocusUp="@id/result_description" + android:nextFocusDown="@id/result_recommendations_list" android:orientation="horizontal" android:paddingTop="5dp" android:requiresFadingEdge="horizontal" @@ -525,8 +604,23 @@ https://developer.android.com/design/ui/tv/samples/jet-fit tools:listitem="@layout/cast_item" tools:visibility="visible" /> + + @@ -576,7 +670,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:layout_height="match_parent" android:layout_gravity="end" android:visibility="gone" - tools:visibility="visible"> + tools:visibility="invisible"> + - + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 89355a72..d8406b35 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -533,18 +533,14 @@ android:id="@id/exo_position" android:layout_width="wrap_content" android:layout_height="30dp" - android:layout_gravity="center" - android:layout_marginStart="20dp" - android:gravity="end|center_vertical" + android:gravity="center" android:includeFontPadding="false" android:minWidth="50dp" - android:paddingLeft="4dp" - android:paddingRight="4dp" android:textColor="@android:color/white" android:textSize="14sp" android:textStyle="normal" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="@id/player_pause_play" + app:layout_constraintStart_toEndOf="@id/player_pause_play" tools:text="15:30" /> +30 This will permanently delete %s\nAre you sure? %dm\nremaining + %s\nremaining Ongoing Completed Status @@ -745,7 +746,8 @@ Display a toggle button for screen orientation Enable automatic switching of screen orientation based on video orientation Auto rotate - + Favorite + Unfavorite Unlock CloudStream Lock with Biometrics @@ -758,4 +760,5 @@ Your CloudStream data has been backed up now, although probability of this rare case is very low but all devices behave differently, in case you get locked down from accessing the app in worst case scenario, Clear the app data wholly and restore the backup. Any inconvenience if arrived is deeply regretted. + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b30d7397..0a693769 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -814,6 +814,35 @@ 0dp + + + + @@ -506,8 +506,8 @@ ?attr/colorPrimary - @android:dimen/dialog_min_width_major - @android:dimen/dialog_min_width_minor + @dimen/abc_dialog_min_width_major + @dimen/abc_dialog_min_width_minor @drawable/dialog__window_background From 35e38a53ad7638ef9407c8f710966ea09d6dac8b Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:29:55 +0530 Subject: [PATCH 029/157] refactor: format build date and time and make it copyable (#1002) --- app/build.gradle.kts | 6 +++--- .../ui/settings/SettingsFragment.kt | 17 ++++++++++++----- app/src/main/res/layout/main_settings.xml | 13 +++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c2ba2907..7ba682be 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,9 +70,9 @@ android { val localProperties = gradleLocalProperties(rootDir) buildConfigField( - "String", - "BUILDDATE", - "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" + "long", + "BUILD_DATE", + "${System.currentTimeMillis()}" ) buildConfigField( "String", 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 72e22269..dfa84998 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 @@ -30,6 +30,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import java.io.File +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone class SettingsFragment : Fragment() { companion object { @@ -180,12 +185,14 @@ class SettingsFragment : Fragment() { val appVersion = getString(R.string.app_version) val commitInfo = getString(R.string.commit_hash) - val buildDate = BuildConfig.BUILDDATE + val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, + Locale.getDefault() + ).apply { timeZone = TimeZone.getTimeZone("UTC") + }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") - binding?.buildDate?.text = buildDate - - binding?.appVersionInfo?.setOnLongClickListener{ - clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo") + binding?.buildDate?.text = buildTimestamp + binding?.appVersionInfo?.setOnLongClickListener { + clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo $buildTimestamp") true } } diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index c3bdc17d..2c90d958 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -112,9 +112,9 @@ android:orientation="horizontal"> @@ -123,8 +123,6 @@ android:id="@+id/delimiter0" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" - android:padding="10dp" android:text="•" android:textColor="?attr/textColor" /> @@ -132,7 +130,6 @@ android:id="@+id/commit_hash" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" android:padding="10dp" android:text="@string/commit_hash" android:textColor="?attr/textColor" /> @@ -141,9 +138,6 @@ android:id="@+id/delimiter1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="center" - android:padding="10dp" android:text="•" android:textColor="?attr/textColor" /> @@ -151,10 +145,9 @@ android:id="@+id/build_date" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="center" android:padding="10dp" - android:textColor="?attr/textColor" /> + android:textColor="?attr/textColor" + tools:text="21/03/2024 09:02 pm"/> From 22937424fa7e96119a665bb10668df8cb89f7d35 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:33:04 +0530 Subject: [PATCH 030/157] feat(ui): authenticate first when enabling security settings (#991) --- app/build.gradle.kts | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 14 ++-- .../ui/account/AccountSelectActivity.kt | 14 ++-- .../ui/settings/SettingsAccount.kt | 67 ++++++++++++++----- .../utils/BiometricAuthenticator.kt | 33 ++++----- app/src/main/res/values/strings.xml | 4 +- build.gradle.kts | 8 +-- 7 files changed, 91 insertions(+), 51 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7ba682be..02946e85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -154,7 +154,7 @@ repositories { dependencies { // Testing testImplementation("junit:junit:4.13.2") - testImplementation("org.json:json:20231013") + testImplementation("org.json:json:20240303") androidTestImplementation("androidx.test:core") implementation("androidx.test.ext:junit-ktx:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 67bf19fb..7baac71c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -135,7 +135,10 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -1231,18 +1234,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, changeStatusBarState(isLayout(EMULATOR)) /** Biometric stuff for users without accounts **/ - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val noAccounts = settingsManager.getBoolean( getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 - if (isLayout(PHONE) && authEnabled && noAccounts) { + if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication(this, R.string.biometric_authentication_title, false) - BiometricAuthenticator.promptInfo?.let { promt -> - BiometricAuthenticator.biometricPrompt?.authenticate(promt) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } // hide background while authenticating, Sorry moms & dads 🙏 @@ -1825,6 +1827,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, binding?.navHostFragment?.isInvisible = false } + override fun onAuthenticationError() { + finish() + } + private var backPressedCallback: OnBackPressedCallback? = null private fun attachBackPressedCallback() { 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 index 41aef176..0b0d83db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -23,7 +23,10 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex @@ -48,7 +51,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet ) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 @@ -56,7 +58,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet fun askBiometricAuth() { - if (isLayout(PHONE) && authEnabled) { + if (isLayout(PHONE) && isAuthEnabled(this)) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication( this, @@ -64,8 +66,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet false ) - BiometricAuthenticator.promptInfo?.let { promt -> - BiometricAuthenticator.biometricPrompt?.authenticate(promt) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } } } @@ -189,4 +191,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet override fun onAuthenticationSuccess() { Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity") } + + override fun onAuthenticationError() { + finish() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 298431ee..f0d402da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -12,6 +12,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent @@ -30,6 +31,7 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref @@ -38,13 +40,20 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -class SettingsAccount : PreferenceFragmentCompat() { +class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { companion object { /** Used by nginx plugin too */ fun showLoginInfo( @@ -252,6 +261,31 @@ class SettingsAccount : PreferenceFragmentCompat() { } } + private fun updateAuthPreference(enabled: Boolean) { + val biometricKey = getString(R.string.biometric_key) + + PreferenceManager.getDefaultSharedPreferences(context ?: return).edit() + .putBoolean(biometricKey, enabled).apply() + findPreference(biometricKey)?.isChecked = enabled + } + + override fun onAuthenticationError() { + updateAuthPreference(!isAuthEnabled(context ?: return)) + } + + override fun onAuthenticationSuccess() { + if (isAuthEnabled(context?: return)) { + updateAuthPreference(true) + BackupUtils.backup(activity) + activity?.showBottomDialogText( + getString(R.string.biometric_setting), + getString(R.string.biometric_warning).html() + ) { onDialogDismissedEvent } + } else { + updateAuthPreference(false) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_account) @@ -263,22 +297,25 @@ class SettingsAccount : PreferenceFragmentCompat() { hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - getPref(R.string.biometric_key)?.setOnPreferenceClickListener { - val authEnabled = PreferenceManager.getDefaultSharedPreferences( - context ?: return@setOnPreferenceClickListener false - ) - .getBoolean(getString(R.string.biometric_key), false) + // hide preference on tvs and emulators + getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE) - if (authEnabled) { - BackupUtils.backup(activity) - val title = activity?.getString(R.string.biometric_setting) - val warning = activity?.getString(R.string.biometric_warning) - activity?.showBottomDialogText( - title as String, - warning.html() - ) { onDialogDismissedEvent } + getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (deviceHasPasswordPinLock(ctx)) { + startBiometricAuthentication( + activity?: return@setOnPreferenceClickListener false, + R.string.biometric_authentication_title, + false + ) + promptInfo?.let { + authCallback = this + biometricPrompt?.authenticate(it) + } } - true + + false } val syncApis = diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index de9b9963..c57600ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -12,20 +12,20 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getString import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R object BiometricAuthenticator { + const val TAG = "cs3Auth" private const val MAX_FAILED_ATTEMPTS = 3 private var failedAttempts = 0 - const val TAG = "cs3Auth" - private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricAuthCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { @@ -37,20 +37,12 @@ object BiometricAuthenticator { activity as FragmentActivity, executor, object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) showToast("$errString") Log.e(TAG, "$errorCode") - failedAttempts++ - - if (failedAttempts >= MAX_FAILED_ATTEMPTS) { - failedAttempts = 0 - activity.finish() - } else { - failedAttempts = 0 - activity.finish() - } + authCallback?.onAuthenticationError() + //activity.finish() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { @@ -89,7 +81,6 @@ object BiometricAuthenticator { .setDescription(description) .setAllowedAuthenticators(authFlag) .build() - } else { // for apis < 30 promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -98,7 +89,6 @@ object BiometricAuthenticator { .setDeviceCredentialAllowed(true) .build() } - } else { // fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -114,7 +104,6 @@ object BiometricAuthenticator { var result = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - when (biometricManager?.canAuthenticate( DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK )) { @@ -126,7 +115,6 @@ object BiometricAuthenticator { BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false } - } else { @Suppress("DEPRECATION") when (biometricManager?.canAuthenticate()) { @@ -153,12 +141,11 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - + authCallback = activity as? BiometricAuthCallback if (isBiometricHardWareAvailable()) { authCallback = activity as? BiometricAuthCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } - } else { if (deviceHasPasswordPinLock(activity)) { authCallback = activity as? BiometricAuthCallback @@ -171,7 +158,15 @@ object BiometricAuthenticator { } } + fun isAuthEnabled(ctx: Context):Boolean { + return ctx.let { + PreferenceManager.getDefaultSharedPreferences(ctx) + .getBoolean(getString(ctx, R.string.biometric_key), false) + } + } + interface BiometricAuthCallback { fun onAuthenticationSuccess() + fun onAuthenticationError() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5dae57b..ab56a849 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -249,7 +249,7 @@ Search Library Accounts and Security - Updates and backup + Updates and Backup Info Advanced Search Gives you the search results separated by provider @@ -611,7 +611,7 @@ Tracks Audio tracks Video tracks - Apply on Restart + Restart the app to see changes. Restart Stop Safe mode on diff --git a/build.gradle.kts b/build.gradle.kts index 06af44d0..801a3c0f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() @@ -6,12 +5,9 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.2.1") + classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle.kts files } } @@ -23,7 +19,7 @@ allprojects { } plugins { - id("com.google.devtools.ksp") version "1.9.22-1.0.16" apply false + id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } tasks.register("clean") { From 7db7742c734421e2e350448b6e08c0b4e8cfb1d0 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 00:04:49 +0000 Subject: [PATCH 031/157] chore(locales): fix locale issues --- app/src/main/res/values-af/strings.xml | 2 +- app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-am/strings.xml | 2 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-eo/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-hr/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-kn/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-lt/strings.xml | 2 +- app/src/main/res/values-lv/strings.xml | 2 +- app/src/main/res/values-mk/strings.xml | 2 +- app/src/main/res/values-ml/strings.xml | 2 +- app/src/main/res/values-my/strings.xml | 2 +- app/src/main/res/values-ne/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-nn/strings.xml | 2 +- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-qt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-so/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-tl/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-ur/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 50 files changed, 50 insertions(+), 50 deletions(-) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 5c19185c..45e9a1d4 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -106,4 +106,4 @@ Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s Nuwe episode notifikasie - \ 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 eb2bf74a..4d1fc074 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -614,4 +614,4 @@ في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. - \ No newline at end of file + diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 63f28ba8..7fd3274b 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -108,4 +108,4 @@ ተጨማሪ መረጃ ዓይነቶችን በመጠቀም ይፈልጉ ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ - \ No newline at end of file + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2ce9fd22..3140afeb 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -641,4 +641,4 @@ خطأ في الوصول الي حافظة النسخ، برجاء المحاولة مرة اخرى. تم النسخ! خطأ في عملية النسخ، برجاء نسخ ال logcat و ارساله الى مسؤولين دعم التطبيق. - \ No newline at end of file + diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 530b07c9..f3811d3d 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -352,4 +352,4 @@ وثائقي موقع عنوان مشغل الفيديو بحد أقصى لعدد الأحرف - \ No newline at end of file + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 4a67f8c5..2be08369 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -601,4 +601,4 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. - \ No newline at end of file + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index aa3def8f..867dd4ed 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -229,4 +229,4 @@ আপনার বর্তমান পর্বের অগ্রগতি স্বয়ংক্রিয়ভাবে সিঙ্ক করুন প্লাগইন ডাউনলোড ফিল্টার করতে মোড নির্বাচন করুন লিঙ্ক পুনরায় লোড হয়েছে - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 1a81021a..0cf1bb2c 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -631,4 +631,4 @@ Erro ao acessar a área de transferência. Tente novamente. Nome e URL do repositório Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo. - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8a11823e..519b05b6 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -633,4 +633,4 @@ Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. Zkopírováno! Chyba při přístupu ke schránce, zkuste to prosím znovu. - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7c56787c..5a871217 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -607,4 +607,4 @@ Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. Fehler beim zugriff auf die Zwischenablage, bitte erneut versuchen. Repository Name und URL - \ No newline at end of file + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 69bc390b..a539f374 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -546,4 +546,4 @@ Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη Απενεργοποιημένο Τέλος - \ No newline at end of file + diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 35b04402..275a4bfb 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -127,4 +127,4 @@ Elŝutite Elŝutante Elŝuto Malsukcesite - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d3e90a41..055fc06b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -609,4 +609,4 @@ ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. Error al acceder al portapapeles. Inténtelo de nuevo. - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 85b65919..486f7a00 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -128,4 +128,4 @@ به پایان رسیده باز کردن در مرورگر برنامه‌ریزی برای تماشا - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 35b93ac6..17f6a667 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - \ No newline at end of file + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 2efe1991..ae3105cf 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -164,4 +164,4 @@ Selecciona o modo para filtrar a descarga dos complementos Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. Mostrar actualizacións da aplicación - \ No newline at end of file + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 68bd645e..8ce224b3 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -192,4 +192,4 @@ लिंक पुन्ह खुली वर्तमान पिन दर्ज करें नेटवर्क स्ट्रीम - \ No newline at end of file + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 4c31c274..ea6a80eb 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -629,4 +629,4 @@ Lozinka/PIN autentifikacija Ovaj uređaj ne podržava biometrijsku autentifikaciju Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo. - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index b27f9df4..5533cdc0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -592,4 +592,4 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 6079d47d..d9a10c61 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -630,4 +630,4 @@ Gagal mengakses Papan Klip, mohon coba lagi. disalin! Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi. - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 08a05c35..7b958ad3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -629,4 +629,4 @@ copiato! Errore durante l\'accesso agli Appunti. Riprova. Errore durante la copia. Copia logcat e contatta il supporto dell\'app. - \ No newline at end of file + diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 79c9e276..da2952a0 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -550,4 +550,4 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5c80d77e..acb2cfc3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,4 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 - \ No newline at end of file + diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 1c9d4e4c..f3fb665d 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -130,4 +130,4 @@ Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ - \ No newline at end of file + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index cb60b51c..1a63050a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -527,4 +527,4 @@ 구독중 구독 %s 구독 취소 %s - \ No newline at end of file + diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cf951ab9..f61bcfc0 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -260,4 +260,4 @@ Ar tikrai norite išeiti\? Pašalinti iš žiūrimų Garso takelis - \ No newline at end of file + diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index deacfdca..49b333e3 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -527,4 +527,4 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - \ No newline at end of file + diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 814a5ed3..fe82a90b 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -591,4 +591,4 @@ Зачестеност на зачувување на бекап Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација Автоматска ротација - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index b4180f23..0ddd8577 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -245,4 +245,4 @@ ഉറവിട പിശക് നിലവിലെ പിൻ നൽകുക ഓഡിയോ ട്രാക്കുകൾ - \ No newline at end of file + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index b29ca920..ef796f9f 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -550,4 +550,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - \ No newline at end of file + diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 97bda0a3..1e23f8af 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -85,4 +85,4 @@ स्रोतहरू स्वचालित बग रिपोर्टिङ असक्षम गर्नुहोस् लागू गर्नुहोस् - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0844c7ec..fc537837 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -608,4 +608,4 @@ Link opnieuw geladen Autoroteer Roteer - \ No newline at end of file + diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 4835bcfb..95c527f9 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -195,4 +195,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - \ No newline at end of file + diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index e599c2b0..724f4a63 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -538,4 +538,4 @@ Bruk Hjelp Profilbakgrunn - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c2e1000a..3e22ba16 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -610,4 +610,4 @@ Błąd dostępu do schowka. Spróbuj ponownie. skopiowano! Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji. - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ff0f952f..b3180fee 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -607,4 +607,4 @@ Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe. Esta janela fechar-se-á após algumas tentativas falhadas. Terá de reiniciar a aplicação. Foi feita uma cópia de segurança dos seus dados CloudStream, embora a probabilidade deste caso raro seja muito baixa, mas todos os dispositivos se comportam de forma diferente. No caso de ficar impedido de aceder à aplicação, na pior das hipóteses, limpe totalmente os dados da aplicação e restaure a cópia de segurança. Lamentamos profundamente qualquer inconveniente. - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 583c6e0e..5de97c7d 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,4 +247,4 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah - \ No newline at end of file + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 224ba880..d7da44b4 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -593,4 +593,4 @@ Adaugă o opțiune de viteză la player Favoriți/te Frecvența de backup - \ No newline at end of file + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 77defab5..16f4449b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -593,4 +593,4 @@ Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров. Добавление настроек скорости в плеер Протестировать всех провайдеров - \ No newline at end of file + diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 1734b39f..ebaaa2ae 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -355,4 +355,4 @@ Maximálny počet znakov v názve prehrávača Spôsobuje problémy, ak je nastavená príliš vysoko v zariadeniach s malým ukladacím priestorom, ako je napríklad Android TV. Frekvencia zálohovania - \ No newline at end of file + diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index a1739399..7b0d2870 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -485,4 +485,4 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka - \ No newline at end of file + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 3c57956e..76508c43 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -604,4 +604,4 @@ Ta bort från favoriter %s \nkvarstår - \ No newline at end of file + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 788afc34..e981d05a 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -119,4 +119,4 @@ போஸ்டர் பிரதான போஸ்டர் %1$s Ep %2$d - \ No newline at end of file + diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 0bdc57c1..b4308eb7 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -265,4 +265,4 @@ Mga Subtitle ng Chromecast Mga setting ng mga subtitle ng Chromecast Maglaro ng Trailer - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 06ff7498..7005fd95 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -656,4 +656,4 @@ kopyalandı! Panoya erişimde hata oluştu. Lütfen tekrar deneyin. Kopyalama hatası. Lütfen logcat\'i kopyalayın ve uygulama desteğiyle iletişime geçin. - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 19424fda..130e50af 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -609,4 +609,4 @@ Назва репозиторію та URL Помилка копіювання, будь ласка, скопіюйте logcat й зверніться до служби підтримки застосунку. Помилка доступу до буфера обміну, спробуйте ще раз. - \ No newline at end of file + diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index c462a7d7..0bcad1cf 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -588,4 +588,4 @@ آغاز پر اکاؤنٹ کا انتخاب چھوڑ دیں ویڈیو واقفیت کی بنیاد پر اسکرین کی سمت بندی کی خودکار سوئچنگ کو فعال کریں خود بخود گھومنا - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 3853f1c8..ad60f597 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -606,4 +606,4 @@ Hiển thị nút xoay màn hình Kích hoạt chế độ xoay màn hình tự động Tự động xoay - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3dc282c0..0e5e34ea 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -637,4 +637,4 @@ 旋轉 根據影片方向自動切換畫面方向 連結已重新載入 - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index a7c4ebc3..2360a7eb 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -651,4 +651,4 @@ \n剩余 测试所有扩展 已复制! - \ No newline at end of file + From 51d91bf9a79be692dab6964ef84c15fd83497b99 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:48:26 +0530 Subject: [PATCH 032/157] feat(ui): add ignore battery optimisation dialog for uniterrupted downloads and notifications (#915) --- .../ui/result/ResultFragmentPhone.kt | 12 ++- .../ui/settings/SettingsGeneral.kt | 19 +++- .../cloudstream3/utils/PowerManagerAPI.kt | 86 +++++++++++++++++++ app/src/main/res/drawable/ic_battery.xml | 12 +++ app/src/main/res/values/strings.xml | 11 +++ app/src/main/res/xml/settings_general.xml | 22 +++-- 6 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt create mode 100644 app/src/main/res/drawable/ic_battery.xml 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 8d0ca37b..fb5160a7 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 @@ -30,7 +30,7 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.CommonActivity +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -61,6 +61,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant @@ -442,8 +443,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { 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) + showToast(txt(message, name), Toast.LENGTH_SHORT) } + context?.let { openBatteryOptimizationSettings(it) } } resultFavorite.setOnClickListener { viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> @@ -457,7 +459,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { 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) + showToast(txt(message, name), Toast.LENGTH_SHORT) } } mediaRouteButton.apply { @@ -465,7 +467,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { alpha = if (chromecastSupport) 1f else 0.3f if (!chromecastSupport) { setOnClickListener { - CommonActivity.showToast( + showToast( R.string.no_chromecast_support_toast, Toast.LENGTH_LONG ) @@ -640,6 +642,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { ), null ) { click -> + context?.let { openBatteryOptimizationSettings(it) } + when (click.action) { DOWNLOAD_ACTION_DOWNLOAD -> { viewModel.handleAction( 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 6cf00375..c3d84867 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 @@ -27,11 +27,15 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout 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.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.isAppRestricted +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.showBatteryOptimizationDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog @@ -45,7 +49,6 @@ import com.lagradost.safefile.SafeFile // Change local language settings in the app. fun getCurrentLocale(context: Context): String { - // val dm = res.displayMetrics val res = context.resources val conf = res.configuration @@ -204,6 +207,20 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + // disable preference on tvs and emulators + getPref(R.string.battery_optimisation_key)?.isEnabled = isLayout(PHONE) + getPref(R.string.battery_optimisation_key)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (isAppRestricted(ctx)) { + showBatteryOptimizationDialog(ctx) + } else { + showToast(R.string.app_unrestricted_toast) + } + + true + } + fun showAdd() { val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt new file mode 100644 index 00000000..27609730 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -0,0 +1,86 @@ +package com.lagradost.cloudstream3.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout + +const val packageName = BuildConfig.APPLICATION_ID +const val TAG = "PowerManagerAPI" + +object BatteryOptimizationChecker { + + fun isAppRestricted(context: Context?): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + return !powerManager.isIgnoringBatteryOptimizations(context.packageName) + } + + return false // below Marshmallow, it's always unrestricted when app is in background + } + + fun openBatteryOptimizationSettings(context: Context) { + if (shouldShowBatteryOptimizationDialog(context)) { + showBatteryOptimizationDialog(context) + } + } + + fun showBatteryOptimizationDialog(context: Context) { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + + try { + context.let { + AlertDialog.Builder(it) + .setTitle(R.string.battery_dialog_title) + .setIcon(R.drawable.ic_battery) + .setMessage(R.string.battery_dialog_message) + .setPositiveButton(R.string.ok) { _, _ -> + intentOpenAppInfo(it) + } + .setNegativeButton(R.string.cancel) { _, _ -> + settingsManager.edit() + .putBoolean(context.getString(R.string.battery_optimisation_key), false) + .apply() + } + .show() + } + } catch (t: Throwable) { + Log.e(TAG, "Error showing battery optimization dialog", t) + } + } + + private fun shouldShowBatteryOptimizationDialog(context: Context): Boolean { + val isRestricted = isAppRestricted(context) + val isOptimizedNotShown = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.battery_optimisation_key), true) + return isRestricted && isOptimizedNotShown && isLayout(PHONE) + } + + private fun intentOpenAppInfo(context: Context) { + val intent = Intent() + try { + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", packageName, null)) + context.startActivity(intent, Bundle()) + } catch (t: Throwable) { + Log.e(TAG, "Unable to invoke any intent", t) + if (t is ActivityNotFoundException) { + showToast("Exception: Activity Not Found") + } else { + showToast(R.string.app_info_intent_error) + } + } + } +} diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml new file mode 100644 index 00000000..24d0a77f --- /dev/null +++ b/app/src/main/res/drawable/ic_battery.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d88489a4..378fa16a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -654,6 +654,17 @@ Are you sure you want to exit\? Yes No + OK + Disable Battery optimization + To ensure uninterrupted downloads and notifications for subscribed + TV shows, CloudStream needs permission to run in background. By pressing "OK", you\'ll be directed to App info. + There, scroll to 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 and set battery usage to 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Please note, this permission + does not mean CS3 will drain your battery. It will only operate in the background when necessary, such as + when receiving notifications or downloading videos from official extensions. If you choose to cancel, + you can adjust this setting later in 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + battery_optimisation + App battery usage is already set to unrestricted + Unable to open CloudStream\'s App info. Downloading app update… Installing app update… Could not install the new version of the app diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index c4900bca..cdda6d85 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -6,10 +6,7 @@ android:title="@string/app_language" android:icon="@drawable/ic_baseline_language_24" /> - + + android:title="@string/title_downloads"> + + + + + + + + Date: Mon, 25 Mar 2024 01:38:39 +0100 Subject: [PATCH 033/157] New TvTypes + General fixes --- .../com/lagradost/cloudstream3/MainAPI.kt | 6 ++- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 2 +- .../ui/home/HomeParentItemAdapter.kt | 4 ++ .../ui/result/ResultViewModel2.kt | 37 ++++++++++++------- app/src/main/res/values/strings.xml | 3 ++ 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 273e267b..ecbdcbbc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -865,7 +865,11 @@ enum class TvType(value: Int?) { AsianDrama(9), Live(10), NSFW(11), - Others(12) + Others(12), + Music(13), + AudioBook(14), + /** Wont load the built in player, make your own interaction */ + CustomMedia(15), } public enum class AutoDownloadMode(val value: Int) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt index 7439bfdf..d90177f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -85,7 +85,7 @@ abstract class BaseAdapter< AsyncDifferConfig.Builder(diffCallback).build() ) - fun submitList(list: List?) { + open fun submitList(list: List?) { // deep copy at least the top list, because otherwise adapter can go crazy mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) }) } 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 fb75e772..4b0360d7 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 @@ -63,6 +63,10 @@ open class ParentItemAdapter( } } + override fun submitList(list: List?) { + super.submitList(list?.sortedBy { it.list.list.isEmpty() }) + } + override fun onUpdateContent( holder: ViewHolderState, item: HomeViewModel.ExpandableHomepageList, 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 c90e01d0..8bf743be 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 @@ -246,6 +246,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { TvType.Live -> R.string.live_singular TvType.Others -> R.string.other_singular TvType.NSFW -> R.string.nsfw_singular + TvType.Music -> R.string.music_singlar + TvType.AudioBook -> R.string.audio_book_singular + TvType.CustomMedia -> R.string.custom_media_singluar } ), yearText = txt(year?.toString()), @@ -1759,20 +1762,28 @@ class ResultViewModel2 : ViewModel() { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = HashMap().apply { putAll(data) } - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - generator?.also { - it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work - ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } - ?.let { index -> - if (index >= 0) - it.goto(index) - } - } ?: return, list + generator?.also { + it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work + ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } + ?.let { index -> + if (index >= 0) + it.goto(index) + } + } + if (currentResponse?.type == TvType.CustomMedia) { + generator?.generateLinks( + clearCache = true, + LoadType.Unknown, + callback = {}, + subtitleCallback = {}) + } else { + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + generator ?: return, list + ) ) - ) + } } ACTION_MARK_AS_WATCHED -> { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 378fa16a..c2136b4d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -764,4 +764,7 @@ Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password. This screen was closed due to multiple failed attempts. Please restart the application. Your CloudStream data has been backed up now. Although the possibility of this is very low, all devices can behave differently. In the rare case, that you get locked out from accessing the app, clear the app data completely and restore from a backup. We are very sorry for any inconvenience arising from this. + Music + Audio Book + Media \ No newline at end of file From 0a24661e4cf301fac304ce6f28f32b718fdc81b1 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 25 Mar 2024 01:48:23 +0100 Subject: [PATCH 034/157] fix latest commit --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 3 +++ 1 file changed, 3 insertions(+) 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 8bf743be..84b8cf48 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 @@ -630,6 +630,9 @@ class ResultViewModel2 : ViewModel() { TvType.Live -> "LiveStreams" TvType.NSFW -> "NSFW" TvType.Others -> "Others" + TvType.Music -> "Music" + TvType.AudioBook -> "AudioBooks" + TvType.CustomMedia -> "Media" } } From a74563d003ba0d4e5f2cc48b527b53c801b90ee4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 8 Apr 2024 04:02:02 +0200 Subject: [PATCH 035/157] Translated using Weblate (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Russian) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Russian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Vietnamese) Currently translated at 75.0% (3 of 4 strings) Translated using Weblate (Vietnamese) Currently translated at 98.7% (688 of 697 strings) Translated using Weblate (French) Currently translated at 96.8% (675 of 697 strings) Translated using Weblate (Arabic (Levantine)) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Persian) Currently translated at 34.7% (242 of 697 strings) Translated using Weblate (Vietnamese) Currently translated at 98.8% (689 of 697 strings) Translated using Weblate (Russian) Currently translated at 97.1% (677 of 697 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Malayalam) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Malayalam) Currently translated at 48.4% (338 of 697 strings) Translated using Weblate (Indonesian) Currently translated at 99.8% (696 of 697 strings) Translated using Weblate (Maltese) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Maltese) Currently translated at 32.1% (224 of 697 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (697 of 697 strings) Added translation using Weblate (Maltese) Translated using Weblate (Spanish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.1% (684 of 697 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Polish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Italian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Czech) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (697 of 697 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Spanish) Currently translated at 99.4% (690 of 694 strings) Translated using Weblate (Odia) Currently translated at 37.5% (258 of 688 strings) Co-authored-by: Andre Costa Co-authored-by: Anonymous Co-authored-by: Argo Carpathians Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com> Co-authored-by: Colgrave Co-authored-by: Fjuro Co-authored-by: Fqwe1 Co-authored-by: Gnkalk Co-authored-by: Herderson Riker Co-authored-by: Hosted Weblate Co-authored-by: Joshua Joseph Co-authored-by: Long Kim Co-authored-by: Massimo Pissarello Co-authored-by: Matthaiks Co-authored-by: Michael John Scerri Co-authored-by: Mika Co-authored-by: Pizza Party Co-authored-by: Rex_sa Co-authored-by: Thanh Co-authored-by: aleksej0R Co-authored-by: gallegonovato Co-authored-by: kaajjo Co-authored-by: maxim Co-authored-by: samwiaba Co-authored-by: Ömer Faruk Sancak Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/mt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/vi/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane --- app/src/main/res/values-ajp/strings.xml | 14 +- app/src/main/res/values-ar/strings.xml | 13 +- app/src/main/res/values-bp/strings.xml | 18 ++- app/src/main/res/values-cs/strings.xml | 12 +- app/src/main/res/values-es/strings.xml | 12 +- app/src/main/res/values-fa/strings.xml | 75 ++++++++++- app/src/main/res/values-fr/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 14 +- app/src/main/res/values-it/strings.xml | 12 +- app/src/main/res/values-ml/strings.xml | 63 +++++++-- app/src/main/res/values-mt/strings.xml | 126 ++++++++++++++++++ app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 16 ++- app/src/main/res/values-pt/strings.xml | 18 ++- app/src/main/res/values-ru/strings.xml | 33 ++++- app/src/main/res/values-tr/strings.xml | 14 +- app/src/main/res/values-uk/strings.xml | 12 +- app/src/main/res/values-vi/strings.xml | 39 ++++-- app/src/main/res/values-zh/strings.xml | 10 +- .../metadata/android/ml-IN/changelogs/2.txt | 1 + .../android/ml-IN/full_description.txt | 10 ++ .../android/ml-IN/short_description.txt | 1 + fastlane/metadata/android/ml-IN/title.txt | 1 + fastlane/metadata/android/mt/changelogs/2.txt | 1 + .../metadata/android/mt/full_description.txt | 10 ++ .../metadata/android/mt/short_description.txt | 1 + fastlane/metadata/android/mt/title.txt | 1 + .../metadata/android/ru-RU/changelogs/2.txt | 1 + .../android/ru-RU/full_description.txt | 10 ++ .../android/ru-RU/short_description.txt | 1 + fastlane/metadata/android/ru-RU/title.txt | 1 + fastlane/metadata/android/vi/title.txt | 2 +- 32 files changed, 477 insertions(+), 71 deletions(-) create mode 100644 app/src/main/res/values-mt/strings.xml create mode 100644 fastlane/metadata/android/ml-IN/changelogs/2.txt create mode 100644 fastlane/metadata/android/ml-IN/full_description.txt create mode 100644 fastlane/metadata/android/ml-IN/short_description.txt create mode 100644 fastlane/metadata/android/ml-IN/title.txt create mode 100644 fastlane/metadata/android/mt/changelogs/2.txt create mode 100644 fastlane/metadata/android/mt/full_description.txt create mode 100644 fastlane/metadata/android/mt/short_description.txt create mode 100644 fastlane/metadata/android/mt/title.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/2.txt create mode 100644 fastlane/metadata/android/ru-RU/full_description.txt create mode 100644 fastlane/metadata/android/ru-RU/short_description.txt create mode 100644 fastlane/metadata/android/ru-RU/title.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 4d1fc074..e4e17942 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -143,7 +143,7 @@ لودينگ… شيل بَعِد مَعلومات - التجديد والنسخات الاحتياطية + التجديدات والنسخات الاحتياطية خبي هيدي الجودات من نتائج التنبيش موقف موقتًا كلود ستريم @@ -448,7 +448,7 @@ التلخيص إِيه الرئيسي - طبّق وقتما سكّر الآپ + سكر الآپ حتى تطبق التغيرات ساعدوني عوز هيدا إذا عم بتبين الترجمة %d ميلي ثانية بعدما لازم ما نلاقا الآپ @@ -614,4 +614,12 @@ في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. - + أوكي + وقف اپتميزايشن بطارية جهازك + بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\" + ما قدرنا نفتح معلومات الآپ تبع \"كلود ستريم\". + موسيقى + أوديو بوك + الميديا + لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 3140afeb..2ac4d973 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -440,7 +440,7 @@ المسارات مسار الصوت مسار الفيديو - تطبيق بعد إعادة التشغيل + أعد تشغيل التطبيق لرؤية التغييرات. الوضع الآمن قيد التشغيل تم إيقاف تشغيل جميع الملحقات بسبب عطل لمساعدتك في العثور على الإضافة التي تسبب مشكلة. عرض بيانات الاعطال @@ -641,4 +641,13 @@ خطأ في الوصول الي حافظة النسخ، برجاء المحاولة مرة اخرى. تم النسخ! خطأ في عملية النسخ، برجاء نسخ ال logcat و ارساله الى مسؤولين دعم التطبيق. - + تعطيل تحسين البطارية + تم ضبط استخدام بطارية التطبيق بالفعل على غير مقيد + غير قادر على فتح معلومات تطبيق CloudStream. + كتاب صوتي + حسناً + لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى استخدام بطارية التطبيق +\nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. + موسيقى + الوسائط + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 0cf1bb2c..e4f47749 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -50,7 +50,7 @@ Fontes Legendas Tentando conectar novamente… - Voltar + Volte Reproduzir episódio Download @@ -145,7 +145,7 @@ Erro no backup de %s Procurar Contas e Segurança - Atualizações e backup + Atualizações e Backup Info Procura Avançada Mostrar resultados separados por fornecedor @@ -473,7 +473,7 @@ Conteúdo +18 Ajuda Processo de configuração de Redo - Não pudemos instalar a nova versão do App + Não foi possível instalar a nova versão do aplicativo instalador de pacotes Organizar por Votação (Alta para Baixa) @@ -541,7 +541,7 @@ MPV Abrindo mistura VLC - Aplicar quando reiniciar + Reinicie o aplicativo para ver as alterações. Visualização info de crash Faixas de áudio Adicionado em (novo para antigo) @@ -631,4 +631,12 @@ Erro ao acessar a área de transferência. Tente novamente. Nome e URL do repositório Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo. - + Para garantir downloads e notificações ininterruptos para programas de TV assinados, o CloudStream precisa de permissão para ser executado em segundo plano. Ao pressionar OK, você será direcionado para as informações do aplicativo. Lá, vá até Uso da bateria do aplicativo e defina o uso da bateria como Irrestrito. Observe que esta permissão não significa que o CS3 irá descarregar sua bateria. Ele só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se você optar por cancelar, poderá ajustar essa configuração posteriormente nas Configurações Gerais. + Ok + Desativar otimização de bateria + O uso da bateria do app já está definido como irrestrito + Não foi possível abrir as informações do aplicativo CloudStream. + Música + Áudio-livro + Mídia + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 519b05b6..cc357373 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -383,7 +383,7 @@ Staženo: %d Zvukové stopy Videostopy - Použít při restartu + Restartujte aplikaci pro použití změn. Bezpečný režim povolen Velikost Autoři @@ -633,4 +633,12 @@ Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. Zkopírováno! Chyba při přístupu ke schránce, zkuste to prosím znovu. - + OK + Využití baterie aplikací je již nastaveno na neomezené + Nepodařilo se otevřít informace o aplikaci CloudStream. + Hudba + Média + Zakažte optimalizace baterie + Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. + Audiokniha + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 055fc06b..bcff5139 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -281,7 +281,7 @@ Omitir configuración Qué quieres ver Poner en MAYÚSCULAS todos los subtítulos - Se aplicarán los cambios al reiniciar la App + Se aplicarán los cambios al reiniciar la App. Reproductor interno Idioma Legacy (método antiguo) @@ -609,4 +609,12 @@ ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. Error al acceder al portapapeles. Inténtelo de nuevo. - + De acuerdo + Desactivar optimización de batería + Música + El uso de la batería de la aplicación está configurado sin restricciones + No se puede abrir la información de la aplicación CloudStream. + Media + Audiolibro + Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. + \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 486f7a00..e9847af6 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -33,16 +33,16 @@ %1$dساعت %2$dدقیقه %dدقیقه پوستر اصلی - تورنت + تورنت‌ها آزاد - مستند ها + مستند‌ها انیمیشن ویدیویی اصلی حداکثر فیلم‌ها سریال های تلویزیونی - درام های آسیایی + درام‌های آسیایی انیمه - کارتونها + کارتون‌ها استفاده شده برنامه بازگشت @@ -52,7 +52,7 @@ اطلاعات بیشتر شرح زبان زیرنویس - زیرنویس + زیرنویس‌ها حذف بارگیری آغاز شد غیرفعال کردن گذارش باگ خودکار @@ -128,4 +128,67 @@ به پایان رسیده باز کردن در مرورگر برنامه‌ریزی برای تماشا - + برای بازنشانی به پیشفرض نگه‌دارید + کتابخانه + درادامه + این فرآیند بطور کامل %s را حذف می‌کند +\nآیا از این کار اطمینان دارید؟ + نام مخزن و نشانی + رونویسی شد! + درباره + قلم + اندازه قلم + برای تغییر تنظیمات بکشید + برای تغییر میزان روشنایی یا صدا در سمت چپ و راست به بالا یا پایین بکشید + بروزرسانی خودکار افزونه + آغاز + زبان برنامه + پخش قسمت + سال + فیلم + سریال + انیمه + رنگ حاشیه متن + دکمه تغییر‌اندازه پخش‌کننده + افزودن گزینه سرعت در پخش‌کننده + بروزرسانی‌ و پشتیبانی + نمایش پوستر از طریق Kitsu + جستجوی پیشرفته + فصل + قسمت + ف + ق + ویدئو + خطای منبع + گزارش + حذف حاشیه سیاه + تنظیمات زیرنویس پخش‌کننده + حساب‌ها و امنیت + نمایش گزارش‌پیوسته 🐈 + پیوند در بریده‌دان رونویسی شد + هیچ پیوندی یافت‌نشد + درام آسیایی + بارگیری خودکار افزونه‌ها + مستند + سرعت پخش + هیچ قسمتی یافت‌نشد + امتیاز: %.1f + قلم‌ها را با گذاشتن در %s وارد کنید + ادامه + بازگردانی به مقدار پیشفرض + −۳۰ + +۳۰ + حذف پرونده + نمایش پیش‌پرده‌ها + قسمت‌ها + %dد +\nباقی‌مانده + گیتهاب + نهان کردن کیفیت ویدئو انتخابی در نتایج جستجو + لغو + %s +\nباقی‌مانده + پیش‌فرض + کارتون + تورنت + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 17f6a667..1370ff2b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -279,7 +279,7 @@ Permissions de stockage manquantes. Veuillez réessayer. Erreur de sauvegarde %s Recherche - Comptes + Comptes et Sécurité Mises à jour et sauvegarde Info Recherche avancée @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index d9a10c61..c3b55ba2 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -138,7 +138,7 @@ Error saat mencadang %s Cari Akun dan Keamanan - Update dan cadangan + Update dan Cadangan Info Pencarian Lanjutan Memberikan hasil pencarian yang dipisahkan berdasarkan provider @@ -432,7 +432,7 @@ Semua Umur %s (Tidak aktif) Trek - Terapkan saat dimuat ulang + Terapkan saat dimuat ulang untuk melihat perubahan. Keterangan Versi Status @@ -630,4 +630,12 @@ Gagal mengakses Papan Klip, mohon coba lagi. disalin! Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi. - + Oke + Matikan pengoptimalan Baterai + Pemakaian baterai untuk aplikasi ini sudah diatur menjadi tidak dibatasi + Gagal membuka info aplikasi CloudStream. + Musik + Buku Audio + Media + Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7b958ad3..01031297 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -432,7 +432,7 @@ Tracce Traccia audio Traccia video - Applica al riavvio + Riavvia app per visualizzare le modifiche. Safe mode attiva Tutte le estensioni sono state disabilitate a causa di un arresto anomalo per aiutarti a trovare l\'estensione che causa il problema. Vedi informazioni del crash @@ -629,4 +629,12 @@ copiato! Errore durante l\'accesso agli Appunti. Riprova. Errore durante la copia. Copia logcat e contatta il supporto dell\'app. - + OK + Disabilita ottimizzazione della batteria + Impossibile aprire le informazioni sull\'app CloudStream. + Media + Per garantire download e notifiche ininterrotti per i programmi TV sottoscritti, CloudStream necessita dell\'autorizzazione per l\'esecuzione in background. Premendo OK, verrai indirizzato alle informazioni sull\'app. Successivamente, scorri fino a \"Utilizzo della batteria\" e imposta l\'utilizzo della batteria su \"Senza restrizioni\". Tieni presente che questa autorizzazione non significa che CS3 scaricherà la batteria. Funzionerà in background solo quando necessario, ad esempio quando si ricevono notifiche o si scaricano video da estensioni ufficiali. Se scegli di annullare, puoi modificare questa impostazione più tardi in \"Impostazioni generali\". + L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" + Musica + Audiolibro + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 0ddd8577..a26f902b 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,14 +3,14 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ് + പുതിയ അപ്ഡേറ്റ്! \n%1$s -> %2$s - CloudStream + ക്ലൗഡ് സ്ട്രീം ഹോം തിരയുക ഡൗൺലോഡ്സ് സെറ്റിങ്‌സ് - തിരയുക + തിരയുക… ടാറ്റ ലഭ്യമല്ല കൂടുതൽ ഓപ്ഷൻസ് അടുത്ത എപ്പിസോഡ് @@ -167,11 +167,11 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - %1$d%2$d - yg5t4r%dujyhtg + %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ് + അധ്യായം%dൽ റിലീസ് ചെയ്യും %1$d മണിക്കൂർ %2$d മിനിറ്റ് - %1$sghj%2$d - rtf:% + %1$sഅധ്യാ%2$d + കാസ്റ്റ്:%s അക്കൗണ്ട് ഉണ്ടാക്കുക പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ് ലൈബ്രറി തിരഞ്ഞെടുക്കുക @@ -179,8 +179,8 @@ ട്രെയിലർ പ്ലേ ചെയ്യുക ലൈവ് സ്ട്രീം പ്ലേ ചെയ്യുക ഫില്ലർ - %d min - ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് കളിക്കുക + %d മിനിറ്റ് + ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കുക അടുത്ത ക്രമരഹിതമായ എപ്പിസോഡ് പോസ്റ്റർ അപ്ഡേറ്റ് ആരംഭിച്ചു @@ -188,7 +188,7 @@ പോസ്റ്റർ ലോഡിംഗ് ഒഴിവാക്കുക തിരയുക %s… - %dm + %dമിനിറ്റ് മടങ്ങിപ്പോവുക പശ്ചാത്തല പ്രിവ്യൂ പോസ്റ്റർ @@ -202,8 +202,6 @@ പൊതു പട്ടിക CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. \n -\nസ്‌കൈ യുകെ ലിമിറ്റഡിലെ ഡോഗ്‌ഷിറ്റ് ആളുകളിൽ നിന്ന് DMCA നീക്കം ചെയ്‌തതിനാൽ 🤮 ഞങ്ങൾക്ക് ആപ്പിൽ റിപ്പോസിറ്ററി സൈറ്റ് ലിങ്ക് ചെയ്യാൻ കഴിയില്ല. -\n \nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. പകർത്തുക എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക @@ -214,7 +212,7 @@ അക്കൗണ്ടുകൾ കൈകാര്യം ചെയ്യുക ഉടൻ വരുന്നു… പുനരാരംഭിക്കുമ്പോൾ പ്രയോഗിക്കുക - അക്കൗണ്ട് എഡിറ്റ് ചെയ്യുക + അക്കൗണ്ട് തിരുത്തുക തെറ്റായ പിൻ. ദയവായി വീണ്ടും ശ്രമിക്കുക. നിർത്തുക ട്രാക്കുകൾ @@ -245,4 +243,41 @@ ഉറവിട പിശക് നിലവിലെ പിൻ നൽകുക ഓഡിയോ ട്രാക്കുകൾ - + ചിത്രം-ഇൻ-ചിത്രം + പുതുക്കിയത് (പഴയത് മുതൽ പുതിയത് വരെ) + റേറ്റിംഗ് (ഉയർന്നത് മുതൽ താഴ്ന്നത്) + പാരമ്പര്യം + വിൻഡോ നിറം + ക്ലിയർ + ലോഗ് + ശുപാർശകൾ കാണിക്കുക + %s ആയി ലോഗിൻ ചെയ്തു + ഇങ്ങനെ അടുക്കുക + അടുക്കുക + തിരുത്തുക + പുതുക്കിയത് (പുതിയത് മുതൽ പഴയത് വരെ) + NSFW + ആപ്പ് അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു… + അപ്ഡേറ്റുകളും ഒപ്പം ബാക്കപ്പും + %s(അപ്രാപ്തമാക്കി) + റേറ്റിംഗ് (താഴ്ന്നത് മുതൽ ഉയർന്നത് വരെ) + വാചക നിറം + ആപ്പിൻ്റെ പുതിയ പതിപ്പ് ഇൻസ്റ്റാൾ ചെയ്യാനായില്ല + പാക്കേജ് ഇൻസ്റ്റാളർ + അക്ഷരമാലാക്രമം (A മുതൽ Z വരെ) + അക്ഷരമാലാക്രമം (Z മുതൽ A വരെ) + ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക. + ചരിത്രം മായ്ക്കുക + ലോഗ്കാറ്റ് കാണിക്കുക 🐈 + നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( +\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. + വീഡിയോ + റിപ്പോസിറ്ററി നാമവും URL ഉം + പകർത്തി! + പുതിയ എപ്പിസോഡ് അറിയിപ്പ് + മറ്റ് വിപുലീകരണങ്ങളിൽ തിരയുക + ഉപശീർഷകം ക്രമീകരണങ്ങൾ + എഡ്ജ് തരം + ഔട്ട്ലൈൻ നിറം + പശ്ചാത്തല നിറം + \ No newline at end of file diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml new file mode 100644 index 00000000..356a2caa --- /dev/null +++ b/app/src/main/res/values-mt/strings.xml @@ -0,0 +1,126 @@ + + + Preferenzi tas-sottotitli + Kulur tal-kitba + Kulur tat-Tieqa + Fittex bl-użu ta \'tipi + Importa fonts billi tpoġġihom ġo %s + Dan il-fornitur huwa torrent, VPN huwa rakkomandat + Atturi: %s + L-episodju %d ha johrog fil + %1$dh %2$dm + %dm + Kartellun + Kartellun + Kartellun tal-episodju + Kartellun Principali + Li jmiss bl\'addoċċ + Ibdel Il-fornitur + veloċità (%.2fx) + Klassifikazzjoni: %.1f + Aġġornament ġdid misjub! +\n%1$s -> %2$s + %d min + CloudStream + Ara bil-CloudStream + Dar + Fittex + Imnizzel + Preferenzi + Fittex… + Fittex%s… + Bla dejta + Iktar Preferenzi + L-episodju li\'jmiss + Ġeneri + Aqsam + Iftah fil-brawser + Brawser + Aqbez it-tagħbija + Tagħbija… + Jaraw + Stenna ftit + Lest + Imwaqqa + Pjana biex tara + Terġa\' tara + Ibda t-trejler + Ibda l-livestream + Stream Torrent + Sorsi + Erġa\' pprova l-konnessjoni… + Mur lura + Ibda l-episodju + Tniżżila ppawzata + Qed jinżlu + Imniżżel + Tniżżil ikkanċellat + Lest it-tniżżil + Beda l-aġġornament + Network stream + Tagħbija tal-Links falliet + Links regaw gew mogħbija + Ħażna Interna + Dub + Ibda + Info + Issettja l-istatus ta-rajtux + Applika + Ikkopja + Għalaq + Neħħi + Issevja + Isem tar-repożitorju u URL + Ikkupjat! + Notifika ta\' episodju ġdid + Fittex f\'estensjonijiet oħra + Uri r-rakkomandazzjonijiet + Veloċità tal-Plejer + Kulur tal-Kontorn + Kulur tal-Isfond + Tip tat-tarf + Elevazzjoni tas-Sottotitolu + Font + Daqs tal-font + Fittex bl-użu ta\' fornituri + %d Benenes mogħtija lil devs + Ebda Benenes mogħtija + Agħżel il-Lingwa Awtomatikament + Niżżel Lingwi + Lingwa tas-sottotitolu + Żomm biex tirrisettja għal default + Kompli Ara + Neħħi + Iktar informazzjoni + \@string/home_play + Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett + Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit. + Deskrizzjoni + Lebda Plot misjub + Lebda Deskrizzjoni misjuba + Uri Logcat 🐈 + ġurnal + Stampa f-istampa + Ikompli d-daqq fi player minjatura fuq apps oħra + %1$s Ep %2$d + %1$dd %2$dh %3$dm + Mur Lura + Ara l\'isfond + Mili + Ibda l-film + Sottotitli + Sut + Ibda l-fajl + Niżżel + Hassar il-fajl + Kompli Nizzel + Ieqaf Nizzel + Iddiżattiva r-rappurtar awtomatiku tal-bugs + Iktar Informazzjoni + Aħbi + Iffiltra l-Bookmarks + Beda t-tniżżil + Bookmarks + Neħħi + Falla t-tniżżil + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index bdc55780..4bf0f565 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,4 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ - + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 3e22ba16..b80c1b79 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -137,7 +137,7 @@ Błąd tworzenia kopii zapasowej %s Szukaj Konta i zabezpieczenia - Aktualizacje i kopia zapasowa + Aktualizacje i kopie zapasowe Informacje Zaawansowane wyszukiwanie Szukaj z podziałem na źródła @@ -278,7 +278,7 @@ Pokaż przycisk do losowania na stronie głównej i w bibliotece Języki rozszerzeń Układ aplikacji - Preferowane media + Preferowane multimedia Włącz NSFW w obsługiwanych rozszerzeniach Kodowanie napisów Źródła @@ -405,7 +405,7 @@ Ścieżki Ścieżki audio Ścieżki wideo - Zastosuj po ponownym uruchomieniu + Uruchom ponownie aplikację, aby zobaczyć zmiany. Tryb bezpieczny włączony Z powodu wystąpienia błędu wszystkie rozszerzenia zostały wyłączone, aby ułatwić wykrycie tego wadliwego. Wyświetl informacje o błędzie @@ -610,4 +610,12 @@ Błąd dostępu do schowka. Spróbuj ponownie. skopiowano! Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji. - + Wyłącz optymalizację akumulatora + Nie można otworzyć informacji o aplikacji CloudStream. + Muzyka + Audiobook + OK + Multimedia + Użycie akumulatora przez aplikację jest już ustawione na nieograniczone + Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b3180fee..82054b6f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -143,7 +143,7 @@ Erro no backup de %s Procurar Contas e segurança - Atualizações e backup + Atualizações e cópias de segurança Info Procura Avançada Mostra resultados separados por fornecedor @@ -458,7 +458,7 @@ Inscrição cancelada em %s Final misto Avaliações (Decrescente) - Aplicar ao reiniciar + Reinicie a aplicação para ver as alterações. Referenciador (opcional) Player oculto - Quantidade de Busca Proxy do GitHub @@ -605,6 +605,14 @@ Autenticação por palavra-passe/PIN A autenticação biométrica não é suportada neste dispositivo Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe. - Esta janela fechar-se-á após algumas tentativas falhadas. Terá de reiniciar a aplicação. - Foi feita uma cópia de segurança dos seus dados CloudStream, embora a probabilidade deste caso raro seja muito baixa, mas todos os dispositivos se comportam de forma diferente. No caso de ficar impedido de aceder à aplicação, na pior das hipóteses, limpe totalmente os dados da aplicação e restaure a cópia de segurança. Lamentamos profundamente qualquer inconveniente. - + Este ecrã foi encerrado devido a várias tentativas falhadas. Reinicie a aplicação. + Os dados do seu CloudStream já foram copiados. Embora a possibilidade de isto acontecer ser muito baixa, todos os dispositivos podem comportar-se de forma diferente. No caso raro de ficar impedido de aceder à aplicação, limpe completamente os dados da aplicação e restaure a partir de uma cópia de segurança. Lamentamos qualquer incómodo causado por esta situação. + OK + A utilização da bateria da aplicação já está definida como sem restrições + Não é possível abrir a informação da aplicação CloudStream. + Música + Livro Aúdio + Multimédia + Desativar a otimização da bateria + Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 16f4449b..a71cc71b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -39,7 +39,7 @@ Заполнитель CloudStream Убирать - %1$s Серия %2$d + %1$s Ep %2$d Смотреть с CloudStream Главная Поиск @@ -148,8 +148,8 @@ Загружена резервная копия Не удалось восстановить данные из %s Отсутствует разрешение на хранение. Пожалуйста попробуйте снова. - Аккаунты - Обновления и резервное копирование + Аккаунты и Безопасность + Обновления и Резервное копирование Информация Расширенный поиск Показывать трейлеры @@ -457,7 +457,7 @@ Загрузить из интернета Загрузка обновления приложения… Недопустимый URL - Будет применено при перезапуске + Перезапустите приложение, чтобы увидеть изменения. Отчеты ошибках Что вы хотите увидеть Смотрите видео на этих языках @@ -593,4 +593,27 @@ Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров. Добавление настроек скорости в плеер Протестировать всех провайдеров - + скопировано! + ОК + Имя репозитория и URL адрес + Ошибка доступа к буферу обмена, пожалуйста, попробуйте ещё раз + Ошибка при копировании, пожалуйста, скопируйте лог и свяжитесь с технической поддержкой. + Нелюбимое + Разблокировать CloudStream + Любимое + Использование батареи приложением уже настроено на неограниченное + Не удается открыть информацию о приложении CloudStream. + Заблокировать биометрией + Музыка + Аудиокнига + Медиа + Разблокируйте приложение с помощью отпечатка пальца, Face ID, PIN-кода, шаблона и пароля. + %s +\nосталось + Отключить оптимизацию батареи + Аутентификация по паролю/PIN-коду + Биометрическая аутентификация на этом устройстве не поддерживается + Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. + Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. + Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 7005fd95..3a5170a3 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -162,7 +162,7 @@ %s yedeklenirken hata Ara Hesaplar ve Güvenlik - Güncellemeler ve yedekleme + Güncellemeler ve Yedekleme Bilgi Gelişmiş arama Arama sonuçlarını sağlayıcıya göre ayırır @@ -466,7 +466,7 @@ Parçalar Ses parçaları Video parçaları - Yeniden başlatmada uygula + Değişiklikleri görmek için uygulamayı yeniden başlatın. Güvenli mod açık Çöküşe neden olan eklentiyi bulmaya yardımcı olabilmek için tüm eklentiler kapatıldı. Çökme bilgisini göster @@ -656,4 +656,12 @@ kopyalandı! Panoya erişimde hata oluştu. Lütfen tekrar deneyin. Kopyalama hatası. Lütfen logcat\'i kopyalayın ve uygulama desteğiyle iletişime geçin. - + Tamam + Pil optimizasyonunu devre dışı bırak + CloudStream\'in Uygulama bilgileri açılamıyor. + Müzik + Sesli Kitap + Medya + Abone olunan TV şovları için kesintisiz indirmeleri ve bildirimleri sağlamak için, CloudStream\'in arka planda çalışmasına izin vermeniz gerekmektedir. Tamam\'a basarak Uygulama bilgilerine yönlendirileceksiniz. Orada, 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 (Uygulama pil kullanımı) kısmına gidip pil kullanımını 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 (Sınırsız) olarak ayarlayın. Bu iznin CS3\'ün pilinizi hızlıca tüketeceği anlamına gelmediğini lütfen unutmayın. Sadece gerektiğinde, resmi eklentilerden bildirim almak veya videoları indirmek gibi durumlarda arka planda çalışacaktır. İptal etmeyi seçerseniz, bu ayarı daha sonra 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 (Genel Ayarlar) bölümünden ayarlayabilirsiniz. + Uygulama pil kullanımı zaten sınırsız olarak ayarlanmış + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 130e50af..981ac19b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -433,7 +433,7 @@ Усі субтитри у верхньому регістрі %s (Вимкнено) Відео доріжки - Застосується при перезавантаженні + Перезапустіть застосунок, щоб побачити зміни. Переглянути інформацію про збій Рейтинг: %s Опис @@ -609,4 +609,12 @@ Назва репозиторію та URL Помилка копіювання, будь ласка, скопіюйте logcat й зверніться до служби підтримки застосунку. Помилка доступу до буфера обміну, спробуйте ще раз. - + Добре + Вимкнути оптимізацію батареї + Щоб забезпечити безперебійне завантаження та повідомлення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши \"Добре\", вас буде перенаправлено до інформації про застосунок. Там прокрутіть до \"Споживання батареї застосунком\" й встановіть використання батареї на \"Без обмежень\". Зверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме вашу батарею. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання повідомлень або завантаження відео з офіційних розширень. Якщо ви вирішите скасувати дозвіл, ви зможете змінити це налаштування пізніше в загальних налаштуваннях. + Споживання батареї застосунком вже налаштовано на без обмежень + Не вдається відкрити інформацію про застосунок CloudStream. + Аудіо книга + Музика + Медіа + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index ad60f597..202af75c 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -123,7 +123,7 @@ Cài đặt phụ đề Phụ đề Chromecast Cài đặt phụ đề Chromecast - Chỉnh tốc độ phim + Tốc độ phát Vuốt để tua nhanh Vuốt sang trái hoặc phải để tua video Vuốt để chỉnh độ sáng và âm lượng @@ -147,8 +147,8 @@ Thiếu quyền truy cập bộ nhớ, hãy thử lại. Lỗi khi sao lưu %s Tìm kiếm - Tài khoản - Cập nhật và sao lưu + Tài khoản và Bảo mật + Cập nhật và Sao lưu Thông tin Tìm kiếm nâng cao Cho phép tìm kiếm theo bộ lọc từng nhà cung cấp @@ -286,7 +286,7 @@ Disclaimer Tổng quan Nút ngẫu nhiên - Hiện nút ngẫu nhiên trên trang chủ + Hiện nút ngẫu nhiên trên Trang chủ và Thư viện Ngôn ngữ nguồn phim Giao diện App Thể loại ưu tiên @@ -307,7 +307,7 @@ Tài khoản Email 127.0.0.1 - Địa chỉ trang web + Địa chỉ trang web­ https://example.com Mã ngôn ngữ (vi) %1$s %2$s @@ -431,7 +431,7 @@ Thêm Âm thanh Chất lượng Video - Áp dụng khi khởi động lại + Áp dụng khi khởi động lại. Chế độ an toàn được bật Đã xảy ra sự cố và chúng tôi đã tự động tắt tất cả các tiện ích mở rộng, hãy tìm và xóa tiện ích mở rộng đang gây ra sự cố. Xem thông tin sự cố @@ -535,7 +535,7 @@ \nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. Đảo ngược lại Đang cập nhật các phim đã đăng kí - Bỏ qua chặn GitHub bằng cách dùng jsDelivr. Có thể gây ra việc cập nhật bị chậm vài ngày. + Bỏ qua chặn đường link GitHub bằng cách dùng jsDelivr. Có thể gây ra việc cập nhật bị chậm vài ngày. Lượng tua thêm được sử dụng khi trình phát ẩn Lượng tua thêm Lượng tua thêm được sử dụng khi trình phát hiện lên @@ -606,4 +606,27 @@ Hiển thị nút xoay màn hình Kích hoạt chế độ xoay màn hình tự động Tự động xoay - + đã sao chép! + Vấn đề truy cập Bảng ghi tạm, Hãy thử lại. + Lỗi sao chép, Hãy sao chép logcat và liên hệ hỗ trợ ứng dụng. + Yêu thích + OK + Vô hiệu Tối ưu pin + Không thể mở thông tin ứng dụng của CloudStream. + Không thích + Mở khóa Cloudstream + Nhạc + Sách nói + Khóa với sinh trắc học + %s +\ncòn lại + Xác thực bằng sinh trắc học không được hỗ trợ trên thiết bị này + Mật khẩu/PIN Xác thực + Dữ liệu CloudStream của bạn đã được sao lưu. Dù khả năng rất thấp, nhưng mỗi thiết bị có thể hoạt động khác nhau. Trong trường hợp thiểu số, bạn sẽ bị khóa khỏi ứng dụng, hãy xóa dữ liệu ứng dụng và khởi tạo từ bản sao lưu. Chúng tôi rất xin lỗi vì bất kỳ sự bất tiện nào. + Mở khóa ứng dụng bằng Vân tay, Khuôn mặt, PIN, Hình vẽ và Mật khẩu. + Màn hình bị đóng sau nhiều lần thử thất bại. Hãy khởi động lại ứng dụng. + Phần kiểm thử này chỉ dành cho nhà phát triển và không xác nhận hay từ chối việc hoạt động của nguồn phim. + Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn + …  +\n——— + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2360a7eb..4423f20f 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -651,4 +651,12 @@ \n剩余 测试所有扩展 已复制! - + 访问剪贴板出错,请重试。 + 应用程序电池使用量已设置为不受限制 + 有声书 + 媒体 + 禁用电池最佳化 + 音乐 + 无法打开 CloudStream 的应用程序信息。 + 使用指纹、面部 ID、PIN 码、图案和密码解锁应用程序。 + \ No newline at end of file diff --git a/fastlane/metadata/android/ml-IN/changelogs/2.txt b/fastlane/metadata/android/ml-IN/changelogs/2.txt new file mode 100644 index 00000000..f7523831 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/changelogs/2.txt @@ -0,0 +1 @@ +-ചേഞ്ച്ലോഗ് ചേർത്തു! diff --git a/fastlane/metadata/android/ml-IN/full_description.txt b/fastlane/metadata/android/ml-IN/full_description.txt new file mode 100644 index 00000000..218f9f98 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/full_description.txt @@ -0,0 +1,10 @@ +ക്ലൗഡ് സ്ട്രീം-3 സിനിമകൾ, ടിവി സീരീസ്, ആനിമേഷൻ എന്നിവ സ്ട്രീം ചെയ്യാനും ഡൗൺലോഡ് ചെയ്യാനും നിങ്ങളെ അനുവദിക്കുന്നു. + +പരസ്യങ്ങളും അനലിറ്റിക്‌സും കൂടാതെ ആപ്പ് വരുന്നു ഒപ്പം +ഒന്നിലധികം ട്രെയിലർ, മൂവി സൈറ്റുകൾ എന്നിവയും മറ്റും പിന്തുണയ്ക്കുന്നു, ഉദാഹരണം + +ബുക്ക്മാർക്കുകൾ + +ഉപശീർഷകം ഡൗൺലോഡുകൾ + +ക്രോംകാസ്റ്റ് പിന്തുണ diff --git a/fastlane/metadata/android/ml-IN/short_description.txt b/fastlane/metadata/android/ml-IN/short_description.txt new file mode 100644 index 00000000..f12fe5b5 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/short_description.txt @@ -0,0 +1 @@ +സ്ട്രീം ഒപ്പം ഡൗൺലോഡ് സിനിമകളും, ടിവി സീരീസുകളും, ആനിമേഷനും . diff --git a/fastlane/metadata/android/ml-IN/title.txt b/fastlane/metadata/android/ml-IN/title.txt new file mode 100644 index 00000000..8e89348a --- /dev/null +++ b/fastlane/metadata/android/ml-IN/title.txt @@ -0,0 +1 @@ +ക്ലൗഡ് സ്ട്രീം diff --git a/fastlane/metadata/android/mt/changelogs/2.txt b/fastlane/metadata/android/mt/changelogs/2.txt new file mode 100644 index 00000000..66bbca8f --- /dev/null +++ b/fastlane/metadata/android/mt/changelogs/2.txt @@ -0,0 +1 @@ +- Changelog miżjud! diff --git a/fastlane/metadata/android/mt/full_description.txt b/fastlane/metadata/android/mt/full_description.txt new file mode 100644 index 00000000..da507aae --- /dev/null +++ b/fastlane/metadata/android/mt/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 iħallik tistrimja u tniżżel Films, Serje TV u Anime. + +L-app tiġi mingħajr reklami u analytics u +jappoġġja siti multipli ta' trejlers u films, u aktar, eż. + +Bookmarks + +Downloads tas-sottotitli + +Appoġġ tal-Chromecast diff --git a/fastlane/metadata/android/mt/short_description.txt b/fastlane/metadata/android/mt/short_description.txt new file mode 100644 index 00000000..542b8614 --- /dev/null +++ b/fastlane/metadata/android/mt/short_description.txt @@ -0,0 +1 @@ +Tistrimja u tniżżel films, serje tat-TV u Anime. diff --git a/fastlane/metadata/android/mt/title.txt b/fastlane/metadata/android/mt/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/mt/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/ru-RU/changelogs/2.txt b/fastlane/metadata/android/ru-RU/changelogs/2.txt new file mode 100644 index 00000000..4b9464b6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/2.txt @@ -0,0 +1 @@ +- Добавлен список изменений! diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 00000000..1790888e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 позволяет транслировать и скачивать фильмы, сериалы и аниме. + +Приложение поставляется без рекламы и аналитики и +поддерживает множество сайтов с трейлерами и фильмами, а также многое другое, например + +Книжные закладки + +Загрузка субтитров + +Поддержка Chromecast diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 00000000..a43bc8a1 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Транслируйте и скачивайте фильмы, сериалы и аниме. diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000..3c0406a6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +Облачный поток diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt index dde89d58..0afff90c 100644 --- a/fastlane/metadata/android/vi/title.txt +++ b/fastlane/metadata/android/vi/title.txt @@ -1 +1 @@ -CloudStream +double_tap_seek_time_key2 From d8f89df16363b17945b24c77f5777bdeb5d068bc Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 10 Apr 2024 17:14:47 +0200 Subject: [PATCH 036/157] Show player controls on pressing Pad Down (#1031) --- .../com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 1 + 1 file changed, 1 insertion(+) 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 d79c44b7..56983190 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 @@ -1159,6 +1159,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_UP -> { if (!isShowing) { onClickChange() From ff0dea3fbb4d17e05d0077db55407981b8b83abd Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 10 Apr 2024 17:16:04 +0200 Subject: [PATCH 037/157] Fix focus for Tracks selection on TV (#1030) --- .../main/res/layout/player_select_tracks.xml | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/app/src/main/res/layout/player_select_tracks.xml b/app/src/main/res/layout/player_select_tracks.xml index d32e1b4e..94e09d60 100644 --- a/app/src/main/res/layout/player_select_tracks.xml +++ b/app/src/main/res/layout/player_select_tracks.xml @@ -38,21 +38,20 @@ android:requiresFadingEdge="vertical" android:id="@+id/video_tracks_list" android:layout_width="match_parent" - android:layout_height="match_parent" android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/audio_tracks_holder" + tools:listitem="@layout/sort_bottom_single_choice" /> + android:id="@+id/audio_tracks_holder" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="50" + android:orientation="vertical"> @@ -107,17 +106,16 @@ + android:requiresFadingEdge="vertical" + android:id="@+id/auto_tracks_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + android:background="?attr/primaryBlackBackground" + android:nextFocusRight="@id/apply_btt" + android:nextFocusLeft="@id/video_tracks_list" + tools:listfooter="@layout/sort_bottom_footer_add_choice" + tools:listitem="@layout/sort_bottom_single_choice" /> @@ -132,11 +130,12 @@ + style="@style/WhiteButton" + android:layout_gravity="center_vertical|end" + android:text="@string/sort_apply" + android:id="@+id/apply_btt" + android:nextFocusLeft="@id/auto_tracks_list" + android:layout_width="wrap_content" /> Date: Wed, 10 Apr 2024 20:54:15 +0530 Subject: [PATCH 038/157] Created vtbe and EPlay Extractor (#1014) --- .../cloudstream3/extractors/EPlay.kt | 28 +++++++++++++ .../lagradost/cloudstream3/extractors/Vtbe.kt | 40 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 6 ++- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt new file mode 100644 index 00000000..565a2680 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson + +open class EPlayExtractor : ExtractorApi() { + override var name = "EPlay" + override var mainUrl = "https://eplayvid.net" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url).document + val trueUrl = response.select("source").attr("src") + return listOf( + ExtractorLink( + this.name, + this.name, + trueUrl, + mainUrl, + getQualityFromName(""), // this needs to be auto + false + ) + ) + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt new file mode 100644 index 00000000..65af01ec --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -0,0 +1,40 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.JsUnpacker +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URI + + +open class Vtbe : ExtractorApi() { + override var name = "Vtbe" + override var mainUrl = "https://vtbe.to" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url,referer=mainUrl).document + val extractedpack =response.selectFirst("script:containsData(function(p,a,c,k,e,d))")?.data().toString() + JsUnpacker(extractedpack).unpack()?.let { unPacked -> + Regex("sources:\\[\\{file:\"(.*?)\"").find(unPacked)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + this.name, + this.name, + link, + referer ?: "", + Qualities.Unknown.value, + URI(link).path.endsWith(".m3u8") + ) + ) + } + } + return null + } +} 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 637f65b9..e5d82d39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -217,6 +217,8 @@ import com.lagradost.cloudstream3.extractors.Zorofile import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub +import com.lagradost.cloudstream3.extractors.EPlayExtractor +import com.lagradost.cloudstream3.extractors.Vtbe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -864,7 +866,9 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), - EmturbovidExtractor() + EmturbovidExtractor(), + Vtbe(), + EPlayExtractor() ) From ffa7b0248a86b8e2dcc8aa13c742741d7ff99b6d Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:26:36 +0000 Subject: [PATCH 039/157] chore(locales): fix locale issues --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 1 + app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ml/strings.xml | 2 +- app/src/main/res/values-mt/strings.xml | 4 ++-- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 20 files changed, 21 insertions(+), 20 deletions(-) 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 c3d84867..ff891c43 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 @@ -98,6 +98,7 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), + Triple("", "Malti", "mt"), Triple("", "ဗမာစာ", "my"), Triple("", "नेपाली", "ne"), Triple("", "Nederlands", "nl"), diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index e4e17942..734d5644 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -622,4 +622,4 @@ أوديو بوك الميديا لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. - \ No newline at end of file + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2ac4d973..8681398d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -650,4 +650,4 @@ \nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. موسيقى الوسائط - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index e4f47749..40847edf 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -639,4 +639,4 @@ Música Áudio-livro Mídia - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index cc357373..0a8cf997 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -641,4 +641,4 @@ Zakažte optimalizace baterie Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. Audiokniha - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index bcff5139..20484cd9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -617,4 +617,4 @@ Media Audiolibro Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index e9847af6..db432a61 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -191,4 +191,4 @@ پیش‌فرض کارتون تورنت - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1370ff2b..77c3db15 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index c3b55ba2..d537a1d5 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -638,4 +638,4 @@ Buku Audio Media Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 01031297..040b0f31 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -637,4 +637,4 @@ L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" Musica Audiolibro - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index a26f902b..279f5511 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -280,4 +280,4 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം - \ No newline at end of file + diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml index 356a2caa..b2c0356a 100644 --- a/app/src/main/res/values-mt/strings.xml +++ b/app/src/main/res/values-mt/strings.xml @@ -92,7 +92,7 @@ Kompli Ara Neħħi Iktar informazzjoni - \@string/home_play + @string/home_play Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit. Deskrizzjoni @@ -123,4 +123,4 @@ Bookmarks Neħħi Falla t-tniżżil - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 4bf0f565..bdc55780 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,4 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b80c1b79..c61f0104 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -618,4 +618,4 @@ Multimedia Użycie akumulatora przez aplikację jest już ustawione na nieograniczone Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 82054b6f..06e2352c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -615,4 +615,4 @@ Multimédia Desativar a otimização da bateria Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. - \ No newline at end of file + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a71cc71b..cf456f56 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -616,4 +616,4 @@ Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3a5170a3..c3e5959a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -664,4 +664,4 @@ Medya Abone olunan TV şovları için kesintisiz indirmeleri ve bildirimleri sağlamak için, CloudStream\'in arka planda çalışmasına izin vermeniz gerekmektedir. Tamam\'a basarak Uygulama bilgilerine yönlendirileceksiniz. Orada, 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 (Uygulama pil kullanımı) kısmına gidip pil kullanımını 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 (Sınırsız) olarak ayarlayın. Bu iznin CS3\'ün pilinizi hızlıca tüketeceği anlamına gelmediğini lütfen unutmayın. Sadece gerektiğinde, resmi eklentilerden bildirim almak veya videoları indirmek gibi durumlarda arka planda çalışacaktır. İptal etmeyi seçerseniz, bu ayarı daha sonra 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 (Genel Ayarlar) bölümünden ayarlayabilirsiniz. Uygulama pil kullanımı zaten sınırsız olarak ayarlanmış - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 981ac19b..403640b9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -617,4 +617,4 @@ Аудіо книга Музика Медіа - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 202af75c..a12570ad 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -629,4 +629,4 @@ Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn …  \n——— - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 4423f20f..828703d1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -659,4 +659,4 @@ 音乐 无法打开 CloudStream 的应用程序信息。 使用指纹、面部 ID、PIN 码、图案和密码解锁应用程序。 - \ No newline at end of file + From e6c111532dd0db555393c78b94bde5c047a168d0 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 13 Apr 2024 19:51:39 +0200 Subject: [PATCH 040/157] Defaults Play button to first unwatched Episode (#1035) --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 69f8e8aa..6a83f396 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 @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer +import com.lagradost.cloudstream3.ui.player.NEXT_WATCH_EPISODE_PERCENTAGE import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent @@ -782,7 +783,7 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.videoWatchState == VideoWatchState.Watched } + val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f } val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } if (firstUnwatched != null) { From afdc4988ac5fa54060c6fae4dc56abdf7679b08f Mon Sep 17 00:00:00 2001 From: Rushikesh Chavan <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 13 Apr 2024 10:52:08 -0700 Subject: [PATCH 041/157] Extractor: Update Vidplay Extractor (#1036) --- .../main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index b9a07a6d..d5d0fb32 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -66,7 +66,7 @@ open class Vidplay : ExtractorApi() { } private suspend fun callFutoken(id: String, url: String): String? { - val script = app.get("$mainUrl/futoken").text + val script = app.get("$mainUrl/futoken", referer = url).text val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null val a = mutableListOf(k) for (i in id.indices) { From aa8972870ccbaf1362be32ba134115463259fe5a Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sat, 13 Apr 2024 22:45:58 +0000 Subject: [PATCH 042/157] Show download size on videos (#1038) --- .../ui/result/ResultViewModel2.kt | 9 +++++-- .../cloudstream3/utils/ExtractorApi.kt | 26 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) 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 84b8cf48..37a905a7 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 @@ -5,6 +5,7 @@ import android.content.* import android.net.Uri import android.os.Build import android.os.Bundle +import android.text.format.Formatter.formatFileSize import android.util.Log import android.widget.Toast import androidx.annotation.MainThread @@ -20,7 +21,6 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.getCastSession @@ -1280,9 +1280,14 @@ class ResultViewModel2 : ViewModel() { callback: (Pair) -> Unit, ) { loadLinks(result, isVisible = true, type) { links -> + // Could not find a better way to do this + val context = AcraApplication.context postPopup( text, - links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { + links.links.apmap { + val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" + txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") + }) { callback.invoke(links to (it ?: return@postPopup)) } } 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 e5d82d39..5a845326 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -404,9 +404,29 @@ open class ExtractorLink constructor( open val extractorData: String? = null, open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { - val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 - val isDash : Boolean get() = type == ExtractorLinkType.DASH - + val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 + val isDash: Boolean get() = type == ExtractorLinkType.DASH + + // Cached video size + private var videoSize: Long? = null + + /** + * Get video size in bytes with one head request. Only available for ExtractorLinkType.Video + * @param timeoutSeconds timeout of the head request. + */ + suspend fun getVideoSize(timeoutSeconds: Long = 3L): Long? { + // Content-Length is not applicable to other types of formats + if (this.type != ExtractorLinkType.VIDEO) return null + + videoSize = videoSize ?: runCatching { + val response = + app.head(this.url, headers = headers, referer = referer, timeout = timeoutSeconds) + response.headers["Content-Length"]?.toLong() + }.getOrNull() + + return videoSize + } + @JsonIgnore fun getAllHeaders() : Map { if (referer.isBlank()) { From 5db541d7ccdc6e305002e2169fa84c56aa0018ab Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Sun, 14 Apr 2024 02:13:12 +0200 Subject: [PATCH 043/157] feat(ui): added reset button to subtitle delay (#1040) --- .../cloudstream3/ui/player/FullScreenPlayer.kt | 14 ++++++-------- app/src/main/res/layout/subtitle_offset.xml | 7 +++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) 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 56983190..c357ce9c 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 @@ -14,13 +14,7 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.Surface -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager +import android.view.* import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -50,7 +44,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -498,6 +491,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { dialog.dismissSafe(activity) player.seekTime(1L) } + resetBtt.setOnClickListener { + subtitleDelay = 0 + dialog.dismissSafe(activity) + player.seekTime(1L) + } cancelBtt.setOnClickListener { subtitleDelay = beforeOffset dialog.dismissSafe(activity) diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index c17c5eff..d5e303b6 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -113,6 +113,13 @@ + + Music Audio Book Media + Reset \ No newline at end of file From 6df3ef14f66dd3cfc038ee922c563684eb84ce4e Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:07:28 +0000 Subject: [PATCH 044/157] First steps for multiplatform API (#1003) * First steps for multiplatform api * Buildconfig testing * Fix publishing and classes.jar * Update build.gradle.kts --- .idea/gradle.xml | 7 +- app/build.gradle.kts | 34 ++++++++-- .../com/lagradost/cloudstream3/MainAPI.kt | 2 - .../lagradost/cloudstream3/mvvm/Lifecycle.kt | 16 +++++ build.gradle.kts | 8 ++- library/build.gradle.kts | 68 +++++++++++++++++++ library/src/androidMain/AndroidManifest.xml | 2 + .../kotlin/com/lagradost/api/Log.kt | 21 ++++++ .../kotlin/com/lagradost/api/Log.kt | 8 +++ .../com/lagradost/cloudstream3/MainApi.kt | 3 + .../cloudstream3/mvvm/ArchComponentExt.kt | 35 +++------- .../jvmMain/kotlin/com/lagradost/api/Log.kt | 19 ++++++ settings.gradle.kts | 3 +- 13 files changed, 185 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt create mode 100644 library/build.gradle.kts create mode 100644 library/src/androidMain/AndroidManifest.xml create mode 100644 library/src/androidMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt (86%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/api/Log.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c5c0ff3b..d7c08c9c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,16 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02946e85..e07162d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream import java.net.URL @@ -13,6 +14,7 @@ plugins { val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() +var isLibraryDebug = false fun String.execute() = ByteArrayOutputStream().use { baot -> if (project.exec { @@ -103,6 +105,7 @@ android { ) } debug { + isLibraryDebug = true isDebuggable = true applicationIdSuffix = ".debug" proguardFiles( @@ -232,18 +235,37 @@ dependencies { implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + + implementation(project(":library") { + this.extra.set("isDebug", isLibraryDebug) + }) } -tasks.register("androidSourcesJar", Jar::class) { +tasks.register("androidSourcesJar") { archiveClassifier.set("sources") from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources } -// For GradLew Plugin -tasks.register("makeJar", Copy::class) { - from("build/intermediates/compile_app_classes_jar/prereleaseDebug") - into("build") - include("classes.jar") +tasks.register("copyJar") { + from( + "build/intermediates/compile_app_classes_jar/prereleaseDebug", + "../library/build/libs" + ) + into("build/app-classes") + include("classes.jar", "library-jvm*.jar") + // Remove the version + rename("library-jvm.*.jar", "library-jvm.jar") +} + +// Merge the app classes and the library classes into classes.jar +tasks.register("makeJar") { + dependsOn(tasks.getByName("copyJar")) + from( + zipTree("build/app-classes/classes.jar"), + zipTree("build/app-classes/library-jvm.jar") + ) + destinationDirectory.set(layout.buildDirectory) + archivesName = "classes" } tasks.withType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index ecbdcbbc..7b1b5775 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -743,8 +743,6 @@ fun base64Encode(array: ByteArray): String { } } -class ErrorLoadingException(message: String? = null) : Exception(message) - fun MainAPI.fixUrlNull(url: String?): String? { if (url.isNullOrEmpty()) { return null diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt new file mode 100644 index 00000000..3df5197c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3.mvvm + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { it?.let { t -> action(t) } } +} + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { action(it) } +} diff --git a/build.gradle.kts b/build.gradle.kts index 801a3c0f..ab1918fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,8 @@ buildscript { classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") + // Universal build config + classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1") } } @@ -22,6 +24,6 @@ plugins { id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} +//tasks.register("clean") { +// delete(rootProject.layout.buildDirectory) +//} diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 00000000..42a8c943 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,68 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + +plugins { + kotlin("multiplatform") + id("maven-publish") + id("com.android.library") + id("com.codingfeline.buildkonfig") +} + +kotlin { + version = "1.0.0" + androidTarget() + jvm() + + sourceSets { + commonMain.dependencies { + implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser + ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API + Level 25 or Less. */ + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + } + } +} + +repositories { + mavenLocal() + maven("https://jitpack.io") +} + +tasks.withType { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +buildkonfig { + packageName = "com.lagradost.api" + exposeObjectWithName = "BuildConfig" + + defaultConfigs { + val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true + buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString()) + } +} + +android { + compileSdk = 34 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + + defaultConfig { + minSdk = 21 + targetSdk = 33 + } + + // If this is the same com.lagradost.cloudstream3.R stops working + namespace = "com.lagradost.api" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} +publishing { + publications { + withType { + groupId = "com.lagradost.api" + } + } +} \ No newline at end of file diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..568741e5 --- /dev/null +++ b/library/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/library/src/androidMain/kotlin/com/lagradost/api/Log.kt b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..12524411 --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,21 @@ +package com.lagradost.api + +import android.util.Log + +actual object Log { + actual fun d(tag: String, message: String) { + Log.d(tag, message) + } + + actual fun i(tag: String, message: String) { + Log.i(tag, message) + } + + actual fun w(tag: String, message: String) { + Log.w(tag, message) + } + + actual fun e(tag: String, message: String) { + Log.e(tag, message) + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/api/Log.kt b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..4b8e6329 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,8 @@ +package com.lagradost.api + +expect object Log { + fun d(tag: String, message: String) + fun i(tag: String, message: String) + fun w(tag: String, message: String) + fun e(tag: String, message: String) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt new file mode 100644 index 00000000..87ee4815 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -0,0 +1,3 @@ +package com.lagradost.cloudstream3 + +class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 817d7db3..d3b4999a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -1,10 +1,7 @@ package com.lagradost.cloudstream3.mvvm -import android.util.Log -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import com.bumptech.glide.load.HttpException -import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.api.BuildConfig +import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException import kotlinx.coroutines.* import java.io.InterruptedIOException @@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { it?.let { t -> action(t) } } -} - -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { action(it) } -} - sealed class Resource { data class Success(val value: T) : Resource() data class Failure( @@ -158,14 +143,14 @@ fun throwAbleToResource( "Connection Timeout\nPlease try again later." ) } - is HttpException -> { - Resource.Failure( - false, - throwable.statusCode, - null, - throwable.message ?: "HttpException" - ) - } +// is HttpException -> { +// Resource.Failure( +// false, +// throwable.statusCode, +// null, +// throwable.message ?: "HttpException" +// ) +// } is UnknownHostException -> { Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}") } diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..e9a0e6b4 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,19 @@ +package com.lagradost.api + +actual object Log { + actual fun d(tag: String, message: String) { + println("DEBUG $tag: $message") + } + + actual fun i(tag: String, message: String) { + println("INFO $tag: $message") + } + + actual fun w(tag: String, message: String) { + println("WARNING $tag: $message") + } + + actual fun e(tag: String, message: String) { + println("ERROR $tag: $message") + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 17070047..eabd9f0e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "CloudStream" -include(":app") \ No newline at end of file +include(":app") +include(":library") \ No newline at end of file From 9a18ef641136cf9335c830145cf5b1bc4a62f8e3 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 17 Apr 2024 23:48:33 +0200 Subject: [PATCH 045/157] bugfix: fixing regex special chars break it (#1047) --- .../main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt index 153dbd3e..d9f0b382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - p = Pattern.compile("\\b\\w+\\b") + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 From 6cef9f7ea257f4af8ed3f739f79c1d01b1b3b36e Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 20 Apr 2024 22:18:49 +0200 Subject: [PATCH 046/157] Filtering first unwatched episode respects watched state (#1049) --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 6a83f396..13621cda 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 @@ -783,7 +783,10 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f } + val lastWatchedIndex = episodes.value.indexOfLast { ep -> + ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched + } + val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } if (firstUnwatched != null) { From e01ff4d843810467660add2a8464973a673daa08 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 22 Apr 2024 01:13:55 +0200 Subject: [PATCH 047/157] Fix NewPipeExtractor lib path (#1050) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e07162d7..f854865d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -202,7 +202,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers + implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding From 4399a612dfa0672acefc7de17c37884ee64331c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 22 Apr 2024 02:14:36 +0300 Subject: [PATCH 048/157] Update Vidmoly.kt (#1051) --- .../java/com/lagradost/cloudstream3/extractors/Vidmoly.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt index 615cfd74..979fd8c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt @@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - + val headers = mapOf( + "User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + "Sec-Fetch-Dest" to "iframe" + ) val script = app.get( url, + headers = headers, referer = referer, ).document.select("script") .find { it.data().contains("sources:") }?.data() @@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() { @JsonProperty("kind") val kind: String? = null, ) -} \ No newline at end of file +} From 0744189020fb3132ebf0debed899e522ab4df246 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:18:54 +0530 Subject: [PATCH 049/157] feat(ui): show account name and image on main settings page (#1001) --- .../ui/settings/SettingsFragment.kt | 52 ++++++++++++++----- app/src/main/res/drawable/rounded_outline.xml | 13 +++++ app/src/main/res/layout/main_settings.xml | 9 ++-- 3 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/rounded_outline.xml 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 dfa84998..443eeda7 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 @@ -1,13 +1,13 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.StringRes import androidx.core.view.children -import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.preference.Preference @@ -18,12 +18,14 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -133,7 +135,6 @@ class SettingsFragment : Fragment() { val localBinding = MainSettingsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root - //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -141,21 +142,44 @@ class SettingsFragment : Fragment() { activity?.navigate(id, Bundle()) } - // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") + /** used to debug leaks + showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : + ${VideoDownloadManager.downloadProgressEvent.size}") **/ - for (syncApi in accountManagers) { - val login = syncApi.loginInfo() - val pic = login?.profilePicture ?: continue - if (binding?.settingsProfilePic?.setImage( - pic, - errorImageDrawable = HomeFragment.errorProfilePic - ) == true - ) { - binding?.settingsProfileText?.text = login.name - binding?.settingsProfile?.isVisible = true - break + fun hasProfilePictureFromAccountManagers(accountManagers: List): Boolean { + for (syncApi in accountManagers) { + val login = syncApi.loginInfo() + val pic = login?.profilePicture ?: continue + + if (binding?.settingsProfilePic?.setImage( + pic, + errorImageDrawable = HomeFragment.errorProfilePic + ) == true + ) { + binding?.settingsProfileText?.text = login.name + return true // sync profile exists + } } + return false // not syncing } + + // display local account information if not syncing + if (!hasProfilePictureFromAccountManagers(accountManagers)) { + val activity = activity ?: return + val currentAccount = try { + DataStoreHelper.accounts.firstOrNull { + it.keyIndex == DataStoreHelper.selectedKeyIndex + } ?: activity.let { DataStoreHelper.getDefaultAccount(activity) } + + } catch (t: IllegalStateException) { + Log.e("AccountManager", "Activity not found", t) + null + } + + binding?.settingsProfilePic?.setImage(currentAccount?.image) + binding?.settingsProfileText?.text = currentAccount?.name + } + binding?.apply { listOf( settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml new file mode 100644 index 00000000..b85ace8e --- /dev/null +++ b/app/src/main/res/drawable/rounded_outline.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index 2c90d958..0b931843 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -24,7 +24,6 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:padding="20dp" - android:visibility="gone" tools:visibility="visible"> + android:scaleType="centerCrop" + android:foreground="@drawable/rounded_outline" + tools:src="@drawable/profile_bg_orange" + android:contentDescription="@string/account"/> + + tools:text="Quick Brown Fox" /> Date: Mon, 22 Apr 2024 16:59:14 +0200 Subject: [PATCH 050/157] Trakt meta provider for extensions (#1026) --- .../metaproviders/TraktProvider.kt | 430 ++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt new file mode 100644 index 00000000..98e12bcd --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -0,0 +1,430 @@ +package com.lagradost.cloudstream3.metaproviders + +import android.net.Uri +import com.lagradost.cloudstream3.* +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import java.util.Locale +import java.text.SimpleDateFormat +import kotlin.math.roundToInt + +open class TraktProvider : MainAPI() { + override var name = "Trakt" + override val hasMainPage = true + override val providerType = ProviderType.MetaProvider + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + ) + + private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") + + override val mainPage = mainPageOf( + "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now + "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time + "$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now + "$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + + val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + return newHomePageResponse(request.name, results) + } + + private fun MediaDetails.toSearchResponse(): SearchResponse { + + val media = this.media ?: this + val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries + val poster = media.images?.poster?.firstOrNull() + + if (mediaType == TvType.Movie) { + return newMovieSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.Movie, + ) { + posterUrl = fixPath(poster) + } + } else { + return newTvSeriesSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.TvSeries, + ) { + this.posterUrl = fixPath(poster) + } + } + } + + override suspend fun search(query: String): List? { + val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + + return results + } + override suspend fun load(url: String): LoadResponse { + + val data = parseJson(url) + val mediaDetails = data.mediaDetails + val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows" + + val posterUrl = mediaDetails?.images?.poster?.firstOrNull() + val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() + + val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + + val actors = parseJson(resActor).cast?.map { + ActorData( + Actor( + name = it.person?.name!!, + image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500") + ), + roleString = it.character + ) + } + + val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + + val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } + + val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") + val isBollywood = mediaDetails?.country == "in" + + if (data.type == TvType.Movie) { + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + type = data.type.toString(), + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + //jpTitle = later if needed as it requires another network request, + airedDate = mediaDetails?.released + ?: mediaDetails?.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + ).toJson() + + return newMovieLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + dataUrl = linkData.toJson(), + type = if (isAnime) TvType.AnimeMovie else TvType.Movie, + ) { + this.name = mediaDetails.title + this.apiName = "Trakt" + this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } else { + + val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val episodes = mutableListOf() + val seasons = parseJson>(resSeasons) + val seasonsNames = mutableListOf() + + seasons.forEach { season -> + + seasonsNames.add( + SeasonData( + season.number!!, + season.title + ) + ) + + season.episodes?.map { episode -> + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + type = data.type.toString(), + season = episode.season, + episode = episode.number, + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + airedYear = mediaDetails?.year, + lastSeason = seasons.size, + epsTitle = episode.title, + //jpTitle = later if needed as it requires another network request, + date = episode.firstAired, + airedDate = episode.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + isCartoon = isCartoon + ).toJson() + + episodes.add( + Episode( + data = linkData.toJson(), + name = episode.title, + season = episode.season, + episode = episode.number, + posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), + rating = episode.rating?.times(10)?.roundToInt(), + description = episode.overview, + ).apply { + this.addDate(episode.firstAired) + } + ) + } + } + + return newTvSeriesLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + type = if (isAnime) TvType.Anime else TvType.TvSeries, + episodes = episodes + ) { + this.name = mediaDetails.title + this.apiName = "Trakt" + this.type = if (isAnime) TvType.Anime else TvType.TvSeries + this.episodes = episodes + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.showStatus = getStatus(mediaDetails.status) + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.seasonNames = seasonsNames + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } + } + + private suspend fun getApi(url: String) : String { + return app.get( + url = url, + headers = mapOf( + "Content-Type" to "application/json", + "trakt-api-version" to "2", + "trakt-api-key" to traktClientId, + ) + ).toString() + } + + private fun isUpcoming(dateString: String?): Boolean { + return try { + val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dateTime = dateString?.let { format.parse(it)?.time } ?: return false + APIHolder.unixTimeMS < dateTime + } catch (t: Throwable) { + logError(t) + false + } + } + + private fun getStatus(t: String?): ShowStatus { + return when (t) { + "returning series" -> ShowStatus.Ongoing + "continuing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + private fun fixPath(url: String?): String? { + url ?: return null + return "https://$url" + } + + private fun getWidthImageUrl(path: String?, width: String) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + val fileName = Uri.parse(path).lastPathSegment ?: return null + return "https://image.tmdb.org/t/p/${width}/${fileName}" + } + + private fun getOriginalWidthImageUrl(path: String?) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + return getWidthImageUrl(path, "original") + } + + data class Data( + val type: TvType? = null, + val mediaDetails: MediaDetails? = null, + ) + + data class MediaDetails( + @JsonProperty("title") val title: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("tagline") val tagline: String? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("country") val country: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("trailer") val trailer: String? = null, + @JsonProperty("homepage") val homepage: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("votes") val votes: Long? = null, + @JsonProperty("comment_count") val commentCount: Long? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("languages") val languages: List? = null, + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("genres") val genres: List? = null, + @JsonProperty("certification") val certification: String? = null, + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("airs") val airs: Airs? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null + ) + + data class Airs( + @JsonProperty("day") val day: String? = null, + @JsonProperty("time") val time: String? = null, + @JsonProperty("timezone") val timezone: String? = null, + ) + + data class Ids( + @JsonProperty("trakt") val trakt: Int? = null, + @JsonProperty("slug") val slug: String? = null, + @JsonProperty("tvdb") val tvdb: Int? = null, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: Int? = null, + @JsonProperty("tvrage") val tvrage: String? = null, + ) + + data class Images( + @JsonProperty("fanart") val fanart: List? = null, + @JsonProperty("poster") val poster: List? = null, + @JsonProperty("logo") val logo: List? = null, + @JsonProperty("clearart") val clearart: List? = null, + @JsonProperty("banner") val banner: List? = null, + @JsonProperty("thumb") val thumb: List? = null, + @JsonProperty("screenshot") val screenshot: List? = null, + @JsonProperty("headshot") val headshot: List? = null, + ) + + data class People( + @JsonProperty("cast") val cast: List? = null, + ) + + data class Cast( + @JsonProperty("character") val character: String? = null, + @JsonProperty("characters") val characters: List? = null, + @JsonProperty("episode_count") val episodeCount: Long? = null, + @JsonProperty("person") val person: Person? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Person( + @JsonProperty("name") val name: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Seasons( + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("episode_count") val episodeCount: Int? = null, + @JsonProperty("episodes") val episodes: List? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class TraktEpisode( + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("comment_count") val commentCount: Int? = null, + @JsonProperty("episode_type") val episodeType: String? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("number_abs") val numberAbs: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class LinkData( + val id: Int? = null, + val imdbId: String? = null, + val tvdbId: Int? = null, + val type: String? = null, + val season: Int? = null, + val episode: Int? = null, + val aniId: String? = null, + val animeId: String? = null, + val title: String? = null, + val year: Int? = null, + val orgTitle: String? = null, + val isAnime: Boolean = false, + val airedYear: Int? = null, + val lastSeason: Int? = null, + val epsTitle: String? = null, + val jpTitle: String? = null, + val date: String? = null, + val airedDate: String? = null, + val isAsian: Boolean = false, + val isBollywood: Boolean = false, + val isCartoon: Boolean = false, + ) +} \ No newline at end of file From e6b9d621f96beba6e427aa092d09bb448caf8d93 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:00:27 +0200 Subject: [PATCH 051/157] feat(ui): added option to reset sub delay (#1041) --- .../ui/player/AbstractPlayerFragment.kt | 14 ++++++++------ .../lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) 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 cfa6682d..0865b220 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 @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent @@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.DefaultTimeBar -import androidx.media3.ui.PlayerView -import androidx.media3.ui.SubtitleView -import androidx.media3.ui.TimeBar +import androidx.media3.ui.* import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar @@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment( is VideoEndedEvent -> { context?.let { ctx -> + // Resets subtitle delay on ended video + player.setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(ctx) ?.getBoolean( 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 210bfdca..31adbc87 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 @@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { + // Resets subtitle delay on ended video + setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) ?.getBoolean( From e2946cad6b0eb2ef602174f8da38ab1a289ac8e2 Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Sun, 28 Apr 2024 00:00:40 +0800 Subject: [PATCH 052/157] Added Vidguard Extractor (#1053) --- .../cloudstream3/extractors/Vidguard.kt | 101 ++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 4 +- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt new file mode 100644 index 00000000..230a9e1a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -0,0 +1,101 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE +import com.lagradost.cloudstream3.utils.Qualities +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeJSON +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.Scriptable +import java.util.Base64 + +open class Vidguardto : ExtractorApi() { + override val name = "Vidguard" + override val mainUrl = "https://vidguard.to" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url) + val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data() + resc?.let { + val jsonStr2 = AppUtils.parseJson(runJS2(it)) + val watchlink = sigDecode(jsonStr2.stream) + + callback.invoke( + ExtractorLink( + this.name, + name, + watchlink, + this.mainUrl, + Qualities.Unknown.value, + INFER_TYPE + ) + ) + } + } + + private fun sigDecode(url: String): String { + val sig = url.split("sig=")[1].split("&")[0] + var t = "" + for (v in sig.chunked(2)) { + val byteValue = Integer.parseInt(v, 16) xor 2 + t += byteValue.toChar() + } + val padding = when (t.length % 4) { + 2 -> "==" + 3 -> "=" + else -> "" + } + val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8)) + t = String(decoded).dropLast(5).reversed() + val charArray = t.toCharArray() + for (i in 0 until charArray.size - 1 step 2) { + val temp = charArray[i] + charArray[i] = charArray[i + 1] + charArray[i + 1] = temp + } + val modifiedSig = String(charArray).dropLast(5) + return url.replace(sig, modifiedSig) + } + + private fun runJS2(hideMyHtmlContent: String): String { + Log.d("runJS", "start") + val rhino = Context.enter() + rhino.initSafeStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initSafeStandardObjects() + scope.put("window", scope, scope) + var result = "" + try { + Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent") + rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null) + val svgObject = scope.get("svg", scope) + result = if (svgObject is NativeObject) { + NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString() + } else { + Context.toString(svgObject) + } + Log.d("runJS", "Result: $result") + } catch (e: Exception) { + Log.e("runJS", "Error executing JavaScript", e) + } finally { + Context.exit() + } + return result + } + + data class SvgObject( + val stream: String, + val hash: String + ) +} 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 5a845326..592dc6f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -186,6 +186,7 @@ import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidguardto import com.lagradost.cloudstream3.extractors.VidhideExtractor import com.lagradost.cloudstream3.extractors.Vidmoly import com.lagradost.cloudstream3.extractors.Vidmolyme @@ -888,7 +889,8 @@ val extractorApis: MutableList = arrayListOf( StreamWishExtractor(), EmturbovidExtractor(), Vtbe(), - EPlayExtractor() + EPlayExtractor(), + Vidguardto() ) From 004c481a5eb8ac8bb0c5a486f2e1f5b35e414f52 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 27 Apr 2024 19:11:22 +0300 Subject: [PATCH 053/157] feat(ui): Episode Air date & Upcoming countdown (#1058) --- .../cloudstream3/ui/result/EpisodeAdapter.kt | 34 ++++++++++++++++++- .../cloudstream3/ui/result/ResultFragment.kt | 5 ++- .../ui/result/ResultViewModel2.kt | 6 ++-- app/src/main/res/drawable/hourglass_24.xml | 9 +++++ .../main/res/layout/result_episode_large.xml | 23 +++++++++++-- app/src/main/res/values/strings.xml | 1 + 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/hourglass_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index fad349c8..2019aa50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -9,9 +9,11 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent @@ -23,6 +25,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -104,7 +108,7 @@ class EpisodeAdapter( override fun getItemViewType(position: Int): Int { val item = getItem(position) - return if (item.poster.isNullOrBlank()) 0 else 1 + return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 } @@ -260,6 +264,33 @@ class EpisodeAdapter( } } + if (card.airDate != null) { + val isUpcoming = unixTimeMS < card.airDate + + if (isUpcoming) { + episodePlayIcon.isVisible = false + episodeUpcomingIcon.isVisible = !episodePoster.isVisible + episodeDate.setText( + txt( + R.string.episode_upcoming_format, + secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + ) + ) + } else { + episodeUpcomingIcon.isVisible = false + + val formattedAirDate = SimpleDateFormat.getDateInstance( + DateFormat.LONG, + Locale.getDefault() + ).apply { + }.format(Date(card.airDate)) + + episodeDate.setText(txt(formattedAirDate)) + } + } else { + episodeDate.isVisible = false + } + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) @@ -271,6 +302,7 @@ class EpisodeAdapter( } } } + itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } 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 a1574eec..1d3f5a08 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 @@ -50,6 +50,7 @@ data class ResultEpisode( val videoWatchState: VideoWatchState, /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -85,6 +86,7 @@ fun buildResultEpisode( tvType: TvType, parentId: Int, totalEpisodeIndex: Int? = null, + airDate: Long? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -107,7 +109,8 @@ fun buildResultEpisode( tvType, parentId, videoWatchState, - totalEpisodeIndex + totalEpisodeIndex, + airDate, ) } 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 37a905a7..499fced2 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 @@ -2277,7 +2277,8 @@ class ResultViewModel2 : ViewModel() { fillers.getOrDefault(episode, false), loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = i.date ) val season = eps.seasonIndex ?: 0 @@ -2326,7 +2327,8 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = episode.date ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml new file mode 100644 index 00000000..7bd1ebbd --- /dev/null +++ b/app/src/main/res/drawable/hourglass_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 76e8c434..e5a6881a 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -43,14 +43,26 @@ android:foreground="?android:attr/selectableItemBackgroundBorderless" android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> + tools:src="@drawable/example_poster" + tools:visibility="invisible"/> + android:src="@drawable/play_button" + tools:visibility="invisible"/> + + + + Episodes %1$d-%2$d %1$d %2$s + Upcoming in %s S E No Episodes found From 138e1a1f0ea4515c33274ac4fa3805e9595dd85e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Apr 2024 04:40:15 +0800 Subject: [PATCH 054/157] Don't check year when checking duplicates if year is empty (#1060) Some sources don't use year which makes this not match when it really should match --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 499fced2..de339aee 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 @@ -1099,13 +1099,14 @@ class ResultViewModel2 : ViewModel() { val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> val librarySyncData = it.syncData + val yearCheck = year == it.year || year == null || it.year == null 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 } + { normalizedName == normalizeString(it.name) && yearCheck } ) checks.any { it() } From ff1ffbeb836a1bc94d002044ba1863e93fd654dc Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Mon, 29 Apr 2024 03:42:38 +0800 Subject: [PATCH 055/157] Update Voe.kt (#1062) --- .../lagradost/cloudstream3/extractors/Voe.kt | 66 ++++++++++++++++--- .../cloudstream3/utils/ExtractorApi.kt | 10 ++- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 2c6998de..67fd7eea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,19 +1,46 @@ package com.lagradost.cloudstream3.extractors +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class Tubeless : Voe() { - override var mainUrl = "https://tubelessceliolymph.com" + override val name = "Tubeless" + override val mainUrl = "https://tubelessceliolymph.com" +} + +class Simpulumlamerop : Voe() { + override val name = "Simplum" + override var mainUrl = "https://simpulumlamerop.com" +} + +class Urochsunloath : Voe() { + override val name = "Uroch" + override var mainUrl = "https://urochsunloath.com" +} + +class Yipsu : Voe() { + override val name = "Yipsu" + override var mainUrl = "https://yip.su" +} + +class MetaGnathTuggers : Voe() { + override val name = "Metagnath" + override val mainUrl = "https://metagnathtuggers.com" } open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" override val requiresReferer = true + + private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex() + private val base64Regex = Regex("'.*'") override suspend fun getUrl( url: String, @@ -25,12 +52,33 @@ open class Voe : ExtractorApi() { val script = res.select("script").find { it.data().contains("sources =") }?.data() val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) - M3u8Helper.generateM3u8( - name, - link ?: return, - "$mainUrl/", - headers = mapOf("Origin" to "$mainUrl/") - ).forEach(callback) - + val videoLinks = mutableListOf() + + if (!link.isNullOrBlank()) { + videoLinks.add( + when { + linkRegex.matches(link) -> link + else -> String(Base64.decode(link, Base64.DEFAULT)) + } + ) + } else { + val link2 = base64Regex.find(script)?.value ?: return + val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val videoLinkDTO = AppUtils.parseJson(decoded) + videoLinkDTO.let { videoLinks.add(it.toString()) } + } + + videoLinks.forEach { videoLink -> + M3u8Helper.generateM3u8( + name, + videoLink, + "$mainUrl/", + headers = mapOf("Origin" to "$mainUrl/") + ).forEach(callback) + } } -} \ No newline at end of file + + data class WcoSources( + @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String, + ) +} 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 592dc6f9..75dceb54 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -83,6 +83,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.MetaGnathTuggers import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz @@ -139,6 +140,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Simpulumlamerop import com.lagradost.cloudstream3.extractors.Solidfiles import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u @@ -175,6 +177,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor import com.lagradost.cloudstream3.extractors.Uqload import com.lagradost.cloudstream3.extractors.Uqload1 import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Urochsunloath import com.lagradost.cloudstream3.extractors.Userload import com.lagradost.cloudstream3.extractors.Userscloud import com.lagradost.cloudstream3.extractors.Uservideo @@ -208,6 +211,7 @@ import com.lagradost.cloudstream3.extractors.Watchx import com.lagradost.cloudstream3.extractors.WcoStream import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.Yipsu import com.lagradost.cloudstream3.extractors.YourUpload import com.lagradost.cloudstream3.extractors.YoutubeExtractor import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor @@ -890,7 +894,11 @@ val extractorApis: MutableList = arrayListOf( EmturbovidExtractor(), Vtbe(), EPlayExtractor(), - Vidguardto() + Vidguardto(), + Simpulumlamerop(), + Urochsunloath(), + Yipsu(), + MetaGnathTuggers() ) From 949b5830b644d3ac23216dd533d40943ab5f6347 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 1 May 2024 20:29:49 +0300 Subject: [PATCH 056/157] feat(ui): Fix downloads focus on TV (#1066) --- .../cloudstream3/ui/download/DownloadChildFragment.kt | 3 ++- .../cloudstream3/ui/download/DownloadFragment.kt | 3 +++ .../java/com/lagradost/cloudstream3/utils/UIHelper.kt | 10 ++++++++++ app/src/main/res/layout/download_child_episode.xml | 5 ++++- app/src/main/res/layout/download_header_episode.xml | 6 +++++- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index c3ec2bbd..d138a1e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.coroutines.Dispatchers @@ -89,9 +90,9 @@ class DownloadChildFragment : Fragment() { setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } + setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index e08eb772..31790b0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -41,6 +41,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI @@ -97,6 +98,8 @@ class DownloadFragment : Fragment() { super.onViewCreated(view, savedInstanceState) hideKeyboard() + binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + observe(downloadsViewModel.noDownloadsText) { binding?.textNoDownloads?.text = it } 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 eedb626a..cb527020 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -45,6 +45,7 @@ import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -58,6 +59,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup @@ -208,6 +210,14 @@ object UIHelper { } } + fun View?.setAppBarNoScrollFlagsOnTV() { + if (isLayout(Globals.TV or EMULATOR)) { + this?.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } + } + } + fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index fd845ee8..4974a027 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -9,6 +9,7 @@ android:layout_height="50dp" android:layout_marginBottom="5dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" android:nextFocusLeft="@id/nav_rail_view" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" @@ -84,7 +85,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/download_child_episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 226c1632..21f79ca6 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -9,6 +9,8 @@ android:layout_marginTop="10dp" android:layout_marginEnd="10dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" app:cardCornerRadius="@dimen/rounded_image_radius"> @@ -71,7 +73,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> \ No newline at end of file From c07e6d3222123ce9b711cafa8827f682f9ad9516 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Thu, 2 May 2024 23:58:32 +0200 Subject: [PATCH 057/157] hotfix: Remove resume information (#1063) --- .../cloudstream3/ui/download/button/PieFetchButton.kt | 4 ++++ .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index a729f33a..f1031c24 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -13,6 +13,8 @@ import androidx.annotation.MainThread import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE @@ -25,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES open class PieFetchButton(context: Context, attributeSet: AttributeSet) : @@ -167,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : this.setPersistentId(card.id) view.setOnClickListener { if (isZeroBytes) { + removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 50a8df02..7d4d5d98 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -187,7 +187,7 @@ object VideoDownloadManager { private val DOWNLOAD_BAD_CONFIG = DownloadStatus(retrySame = false, tryNext = false, success = false) - private const val KEY_RESUME_PACKAGES = "download_resume" + const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume" From d3828eeafed0fd4fbeb32c4d37dee2126296b564 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Thu, 2 May 2024 23:59:05 +0200 Subject: [PATCH 058/157] refact: rename logcat file (#1061) Rename logcat file to prevent override --- .../cloudstream3/ui/settings/SettingsUpdates.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 fb24c185..4aaa5e12 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 @@ -35,6 +35,9 @@ import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStream +import java.lang.System.currentTimeMillis +import java.text.SimpleDateFormat +import java.util.* class SettingsUpdates : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -125,12 +128,12 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { - fileStream = - VideoDownloadManager.setupStream( + fileStream = VideoDownloadManager.setupStream( it.context, - "logcat", + "logcat_${date}", null, "txt", false From c28a3cb9873d64634b1e7bb131ef648ab40fd22e Mon Sep 17 00:00:00 2001 From: RowdyRushya <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 4 May 2024 04:15:34 -0700 Subject: [PATCH 059/157] Extractor: new VidSrcTo extractor (#1044) --- .../cloudstream3/extractors/VidSrcTo.kt | 65 +++++++++++++++++++ .../cloudstream3/extractors/Vidplay.kt | 4 ++ .../metaproviders/TmdbProvider.kt | 2 + .../cloudstream3/utils/ExtractorApi.kt | 2 + 4 files changed, 73 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt new file mode 100644 index 00000000..b9065688 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -0,0 +1,65 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } + } + + private fun DecryptUrl(encUrl: String): String { + var data = encUrl.toByteArray() + data = Base64.decode(data, Base64.URL_SAFE) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + data = cipher.doFinal(data) + return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index d5d0fb32..c5e01552 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key +class AnyVidplay(hostUrl: String) : Vidplay() { + override val mainUrl = hostUrl +} + class MyCloud : Vidplay() { override val name = "MyCloud" override val mainUrl = "https://mcloud.bz" diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index 50301e22..c5b4d453 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() { this.id, episode.episode_number, episode.season_number, + this.name ?: this.original_name, ).toJson(), episode.name, episode.season_number, @@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() { this.id, episodeNum, season.season_number, + this.name ?: this.original_name, ).toJson(), season = season.season_number ) 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 75dceb54..6106845e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -185,6 +185,7 @@ import com.lagradost.cloudstream3.extractors.Vanfem import com.lagradost.cloudstream3.extractors.Vicloud import com.lagradost.cloudstream3.extractors.VidSrcExtractor import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VidSrcTo import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime @@ -876,6 +877,7 @@ val extractorApis: MutableList = arrayListOf( Streamlare(), VidSrcExtractor(), VidSrcExtractor2(), + VidSrcTo(), PlayLtXyz(), AStreamHub(), Vidplay(), From 83c473d9f801cc43c0716453bea79afc539a1fea Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 May 2024 14:16:09 +0300 Subject: [PATCH 060/157] More external Ids in Trakt meta provider (#1075) --- .../cloudstream3/metaproviders/TraktProvider.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 98e12bcd..37c6be1b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -118,8 +118,12 @@ open class TraktProvider : MainAPI() { val linkData = LinkData( id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, type = data.type.toString(), title = mediaDetails?.title, year = mediaDetails?.year, @@ -139,7 +143,6 @@ open class TraktProvider : MainAPI() { type = if (isAnime) TvType.AnimeMovie else TvType.Movie, ) { this.name = mediaDetails.title - this.apiName = "Trakt" this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie this.posterUrl = getOriginalWidthImageUrl(posterUrl) this.year = mediaDetails.year @@ -177,8 +180,12 @@ open class TraktProvider : MainAPI() { val linkData = LinkData( id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, type = data.type.toString(), season = episode.season, episode = episode.number, @@ -220,7 +227,6 @@ open class TraktProvider : MainAPI() { episodes = episodes ) { this.name = mediaDetails.title - this.apiName = "Trakt" this.type = if (isAnime) TvType.Anime else TvType.TvSeries this.episodes = episodes this.posterUrl = getOriginalWidthImageUrl(posterUrl) @@ -406,8 +412,12 @@ open class TraktProvider : MainAPI() { data class LinkData( val id: Int? = null, + val traktId: Int? = null, + val traktSlug: String? = null, + val tmdbId: Int? = null, val imdbId: String? = null, val tvdbId: Int? = null, + val tvrageId: String? = null, val type: String? = null, val season: Int? = null, val episode: Int? = null, From 71bd48f4930d255beabe6f86b7e4057b732dc70e Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 May 2024 14:17:52 +0300 Subject: [PATCH 061/157] feat(ui): Hide Downloads & Settings Back button on TV (#1074) --- .../ui/download/DownloadChildFragment.kt | 11 ++++++++--- .../ui/quicksearch/QuickSearchFragment.kt | 12 ++++++++++-- .../ui/settings/SettingsFragment.kt | 19 ++++++++++++------- app/src/main/res/layout/quick_search.xml | 7 +++---- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index d138a1e6..f54c8698 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -11,6 +11,9 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -86,9 +89,11 @@ class DownloadChildFragment : Fragment() { binding?.downloadChildToolbar?.apply { title = name - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } setAppBarNoScrollFlagsOnTV() } 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 e9e00736..85e20d1c 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 @@ -34,6 +34,9 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownShow @@ -274,8 +277,13 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - binding?.quickSearchBack?.setOnClickListener { - activity?.popCurrentPage() + if (isLayout(PHONE or EMULATOR)) { + binding?.quickSearchBack?.apply { + isVisible = true + setOnClickListener { + activity?.popCurrentPage() + } + } } if (isLayout(TV)) { 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 443eeda7..8ac17928 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 @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.account import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper @@ -84,9 +85,11 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) @@ -98,10 +101,12 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) diff --git a/app/src/main/res/layout/quick_search.xml b/app/src/main/res/layout/quick_search.xml index 12d94aaa..84f2c548 100644 --- a/app/src/main/res/layout/quick_search.xml +++ b/app/src/main/res/layout/quick_search.xml @@ -23,11 +23,10 @@ android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_arrow_back_24" app:tint="@android:color/white" - android:focusable="true" + android:visibility="gone" android:layout_width="25dp" - android:layout_height="wrap_content"> - - + android:layout_height="wrap_content" + tools:visibility="visible"> Date: Sun, 5 May 2024 04:30:42 +0530 Subject: [PATCH 062/157] Updates and Chillx Extractor Updated (#1065) --- .../cloudstream3/extractors/Chillx.kt | 48 ++++++++++--------- .../cloudstream3/extractors/EPlay.kt | 1 - .../lagradost/cloudstream3/extractors/Vtbe.kt | 1 - 3 files changed, 25 insertions(+), 25 deletions(-) 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 f03a5525..26567c7a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.extractors.helper.* import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler -import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper @@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() { override val name = "Chillx" override val mainUrl = "https://chillx.top" override val requiresReferer = true - private var key: String? = null + companion object { + private var key: String? = null + + suspend fun fetchKey(): String { + return if (key != null) { + key!! + } else { + val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") + key = fetch + key!! + } + } + } + + @Suppress("NAME_SHADOWING") override suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val master = Regex("\\s*=\\s*'([^']+)").find( + val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find( app.get( url, - referer = referer ?: "", - headers = mapOf( - "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", - "Accept-Language" to "en-US,en;q=0.5", - ) + referer = url, ).text )?.groupValues?.get(1) - val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - + val key = fetchKey() + val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() + val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex() val matches = subtitlePattern.findAll(subtitles ?: "") val languageUrlPairs = matches.map { matchResult -> val (language, url) = matchResult.destructured @@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() { headers = headers ).forEach(callback) } - + private fun decodeUnicodeEscape(input: String): String { val regex = Regex("u([0-9a-fA-F]{4})") return regex.replace(input) { it.groupValues[1].toInt(16).toChar().toString() } } - - suspend fun getKey() = key ?: fetchKey().also { key = it } - private suspend fun fetchKey(): String { - return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed() - } - data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + + data class Keys( + @JsonProperty("chillx") val key: List ) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt index 565a2680..2cb12e16 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt index 65af01ec..919a9cbd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* From 3874cb9f9d3a2b0894c59346b92fb6eec4fa2b2e Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Thu, 9 May 2024 23:06:33 +0800 Subject: [PATCH 063/157] Update Dailymotion Extractor (#1081) --- .../cloudstream3/extractors/Dailymotion.kt | 23 +++++++++++++------ .../cloudstream3/utils/ExtractorApi.kt | 5 +++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 0df93dc5..2343a92e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import java.net.URL +class Geodailymotion : Dailymotion() { + override val name = "GeoDailymotion" + override val mainUrl = "https://geo.dailymotion.com" +} + open class Dailymotion : ExtractorApi() { override val mainUrl = "https://www.dailymotion.com" override val name = "Dailymotion" override val requiresReferer = false + private val baseUrl = "https://www.dailymotion.com" @Suppress("RegExpSimplifiable") private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex() @@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() { val dmV1st = config.dmInternalData.v1st val dmTs = config.dmInternalData.ts val embedder = config.context.embedder - val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" + val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies) .parsedSafe() ?: return metaData.qualities.forEach { (_, video) -> @@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() { } private fun getEmbedUrl(url: String): String? { - if (url.contains("/embed/")) { - return url - } - val vid = getVideoId(url) ?: return null - return "$mainUrl/embed/video/$vid" + if (url.contains("/embed/") || url.contains("/video/")) { + return url } + if (url.contains("geo.dailymotion.com")) { + val videoId = url.substringAfter("video=") + return "$baseUrl/embed/video/$videoId" + } + return null + } private fun getVideoId(url: String): String? { val path = URL(url).path - val id = path.substringAfter("video/") + val id = path.substringAfter("/video/") if (id.matches(videoIdRegex)) { return id } 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 6106845e..0e4dc870 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn import com.lagradost.cloudstream3.extractors.FileMoonSx import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.Geodailymotion import com.lagradost.cloudstream3.extractors.GMPlayer import com.lagradost.cloudstream3.extractors.Gdriveplayer import com.lagradost.cloudstream3.extractors.Gdriveplayerapi @@ -900,7 +901,9 @@ val extractorApis: MutableList = arrayListOf( Simpulumlamerop(), Urochsunloath(), Yipsu(), - MetaGnathTuggers() + MetaGnathTuggers(), + Geodailymotion(), + ) From f1cc4db89cc6c1a2cd6316e81340206b56a72ad6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 9 May 2024 18:08:18 +0300 Subject: [PATCH 064/157] Show Season number for next airing episode (#1071) --- .../com/lagradost/cloudstream3/MainAPI.kt | 17 +++++++-- .../ui/result/ResultViewModel2.kt | 6 +++- .../main/res/layout/fragment_result_tv.xml | 36 +++++++++---------- app/src/main/res/values/strings.xml | 1 + 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7b1b5775..699159b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1448,11 +1448,24 @@ fun TvType?.isEpisodeBased(): Boolean { return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) } - data class NextAiring( val episode: Int, val unixTime: Long, -) + val season: Int? = null, +) { + /** + * Secondary constructor for backwards compatibility without season. + * TODO Remove this constructor after there is a new stable release and extensions are updated to support season. + */ + constructor( + episode: Int, + unixTime: Long, + ) : this ( + episode, + unixTime, + null + ) +} /** * @param season To be mapped with episode season, not shown in UI if displaySeason is defined 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 de339aee..61b65bc2 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 @@ -197,7 +197,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { else -> null }?.also { - nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) + nextAiringEpisode = when (airing.season) { + + null -> txt(R.string.next_episode_format, airing.episode) + else -> txt(R.string.next_season_episode_format, airing.season, airing.episode) + } } } } diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 2ec2ae0a..893c19ff 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -178,42 +178,40 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:textStyle="bold" tools:text="The Perfect Run The Perfect Run" /> + + - - + android:orientation="horizontal"> + android:layout_marginEnd="5dp" + tools:text="Season 2 Episode 1022 will be released in" /> %1$s Ep %2$d Cast: %s Episode %d will be released in + Season %1$d Episode %2$d will be released in %1$dd %2$dh %3$dm %1$dh %2$dm %dm From ee4d1dedc5adb1be656a05d1ee0f41b11f9d0a84 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 9 May 2024 19:46:54 +0000 Subject: [PATCH 065/157] Add basic fcast support (#1084) --- .../lagradost/cloudstream3/MainActivity.kt | 3 + .../cloudstream3/ui/player/IGenerator.kt | 10 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 2 + .../ui/result/ResultViewModel2.kt | 44 ++++++ .../cloudstream3/utils/ExtractorApi.kt | 13 +- .../cloudstream3/utils/fcast/FcastManager.kt | 135 ++++++++++++++++++ .../cloudstream3/utils/fcast/FcastSession.kt | 60 ++++++++ .../cloudstream3/utils/fcast/Packets.kt | 62 ++++++++ app/src/main/res/values/strings.xml | 3 + 9 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 7baac71c..56322b73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -161,6 +161,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.fcast.FcastManager import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import com.lagradost.safefile.SafeFile @@ -1756,6 +1757,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, runAutoUpdate() } + FcastManager().init(this, false) + APIRepository.dubStatusActive = getApiDubstatusSettings() try { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index af74cb57..c5de1a1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -10,7 +10,8 @@ enum class LoadType { InAppDownload, ExternalApp, Browser, - Chromecast + Chromecast, + Fcast } fun LoadType.toSet() : Set { @@ -29,12 +30,17 @@ fun LoadType.toSet() : Set { ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8 ) - LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet() LoadType.Chromecast -> setOf( ExtractorLinkType.VIDEO, ExtractorLinkType.DASH, ExtractorLinkType.M3U8 ) + LoadType.Fcast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 2019aa50..e4fd0559 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -55,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 +const val ACTION_FCAST = 19 + const val TV_EP_SIZE_LARGE = 400 const val TV_EP_SIZE_SMALL = 300 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) 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 61b65bc2..a32942f6 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 @@ -83,6 +83,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.fcast.FcastManager +import com.lagradost.cloudstream3.utils.fcast.FcastSession +import com.lagradost.cloudstream3.utils.fcast.Opcode +import com.lagradost.cloudstream3.utils.fcast.PlayMessage import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit @@ -1519,6 +1523,13 @@ class ResultViewModel2 : ViewModel() { ) ) } + + if (FcastManager.currentDevices.isNotEmpty()) { + options.add( + txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST + ) + } + options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) for (app in apps) { @@ -1694,6 +1705,39 @@ class ResultViewModel2 : ViewModel() { } } + ACTION_FCAST -> { + val devices = FcastManager.currentDevices.toList() + postPopup( + txt(R.string.player_settings_select_cast_device), + devices.map { txt(it.name) }) { index -> + if (index == null) return@postPopup + val device = devices.getOrNull(index) + + acquireSingleLink( + click.data, + LoadType.Fcast, + txt(R.string.episode_action_cast_mirror) + ) { (result, index) -> + val host = device?.host ?: return@acquireSingleLink + val link = result.links.firstOrNull() ?: return@acquireSingleLink + + FcastSession(host).use { session -> + session.sendMessage( + Opcode.Play, + PlayMessage( + link.type.getMimeType(), + link.url, + headers = mapOf( + "referer" to link.referer, + "user-agent" to USER_AGENT + ) + link.headers + ) + ) + } + } + } + } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, LoadType.Browser, 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 0e4dc870..61cdd26a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -308,7 +308,18 @@ enum class ExtractorLinkType { /** No support at the moment */ TORRENT, /** No support at the moment */ - MAGNET, + MAGNET; + + // See https://www.iana.org/assignments/media-types/media-types.xhtml + fun getMimeType(): String { + return when (this) { + VIDEO -> "video/mp4" + M3U8 -> "application/x-mpegURL" + DASH -> "application/dash+xml" + TORRENT -> "application/x-bittorrent" + MAGNET -> "application/x-bittorrent" + } + } } private fun inferTypeFromUrl(url: String): ExtractorLinkType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt new file mode 100644 index 00000000..9ff5cc08 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt @@ -0,0 +1,135 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdManager.ResolveListener +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe + +class FcastManager { + private var nsdManager: NsdManager? = null + + // Used for receiver + private val registrationListenerTcp = DefaultRegistrationListener() + private fun getDeviceName(): String { + return "${Build.MANUFACTURER}-${Build.MODEL}" + } + + /** + * Start the fcast service + * @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app + */ + fun init(context: Context, registerReceiver: Boolean) = ioSafe { + nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + val serviceType = "_fcast._tcp" + + if (registerReceiver) { + val serviceName = "$APP_PREFIX-${getDeviceName()}" + + val serviceInfo = NsdServiceInfo().apply { + this.serviceName = serviceName + this.serviceType = serviceType + this.port = TCP_PORT + } + + nsdManager?.registerService( + serviceInfo, + NsdManager.PROTOCOL_DNS_SD, + registrationListenerTcp + ) + } + + nsdManager?.discoverServices( + serviceType, + NsdManager.PROTOCOL_DNS_SD, + DefaultDiscoveryListener() + ) + } + + fun stop() { + nsdManager?.unregisterService(registrationListenerTcp) + } + + inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener { + val tag = "DiscoveryListener" + override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode") + } + + override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode") + } + + override fun onDiscoveryStarted(serviceType: String?) { + Log.d(tag, "Discovery started: $serviceType") + } + + override fun onDiscoveryStopped(serviceType: String?) { + Log.d(tag, "Discovery stopped: $serviceType") + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + nsdManager?.resolveService(serviceInfo, object : ResolveListener { + override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { + } + + override fun onServiceResolved(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + currentDevices.add(PublicDeviceInfo(serviceInfo)) + + Log.d( + tag, + "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}" + ) + } + }) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + // May remove duplicates, but net and port is null here, preventing device specific identification + currentDevices.removeAll { + it.rawName == serviceInfo.serviceName + } + + Log.d(tag, "Service lost: ${serviceInfo.serviceName}") + } + } + + companion object { + const val APP_PREFIX = "CloudStream" + val currentDevices: MutableList = mutableListOf() + + class DefaultRegistrationListener : NsdManager.RegistrationListener { + val tag = "DiscoveryService" + override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service registered: ${serviceInfo.serviceName}") + } + + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service registration failed: errorCode=$errorCode") + } + + override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}") + } + + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service unregistration failed: errorCode=$errorCode") + } + } + + const val TCP_PORT = 46899 + } +} + +class PublicDeviceInfo(serviceInfo: NsdServiceInfo) { + val rawName: String = serviceInfo.serviceName + val host: String? = serviceInfo.host.hostAddress + val name = rawName.replace("-", " ") + host?.let { " $it" } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt new file mode 100644 index 00000000..1f33bca4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.safefile.closeQuietly +import java.io.DataOutputStream +import java.net.Socket +import kotlin.jvm.Throws + +class FcastSession(private val hostAddress: String): AutoCloseable { + val tag = "FcastSession" + + private var socket: Socket? = null + @Throws + @WorkerThread + fun open(): Socket { + val socket = Socket(hostAddress, FcastManager.TCP_PORT) + this.socket = socket + return socket + } + + override fun close() { + socket?.closeQuietly() + socket = null + } + + @Throws + private fun acquireSocket(): Socket { + return socket ?: open() + } + + fun ping() { + sendMessage(Opcode.Ping, null) + } + + fun sendMessage(opcode: Opcode, message: T) { + ioSafe { + val socket = acquireSocket() + val outputStream = DataOutputStream(socket.getOutputStream()) + + val json = message?.toJson() + val content = json?.toByteArray() ?: ByteArray(0) + + // Little endian starting from 1 + // https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 + val size = content.size + 1 + + val sizeArray = ByteArray(4) { num -> + (size shr 8 * num and 0xff).toByte() + } + + Log.d(tag, "Sending message with size: $size, opcode: $opcode") + outputStream.write(sizeArray) + outputStream.write(ByteArray(1) { opcode.value }) + outputStream.write(content) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt new file mode 100644 index 00000000..61c00d6e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt @@ -0,0 +1,62 @@ +package com.lagradost.cloudstream3.utils.fcast + +// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 +enum class Opcode(val value: Byte) { + None(0), + Play(1), + Pause(2), + Resume(3), + Stop(4), + Seek(5), + PlaybackUpdate(6), + VolumeUpdate(7), + SetVolume(8), + PlaybackError(9), + SetSpeed(10), + Version(11), + Ping(12), + Pong(13); +} + + +data class PlayMessage( + val container: String, + val url: String? = null, + val content: String? = null, + val time: Double? = null, + val speed: Double? = null, + val headers: Map? = null +) + +data class SeekMessage( + val time: Double +) + +data class PlaybackUpdateMessage( + val generationTime: Long, + val time: Double, + val duration: Double, + val state: Int, + val speed: Double +) + +data class VolumeUpdateMessage( + val generationTime: Long, + val volume: Double +) + +data class PlaybackErrorMessage( + val message: String +) + +data class SetSpeedMessage( + val speed: Double +) + +data class SetVolumeMessage( + val volume: Double +) + +data class VersionMessage( + val version: Long +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bd2426c..a8108623 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -357,6 +357,7 @@ Download error, check storage permissions Chromecast episode Chromecast mirror + Cast mirror Play in app Play in %s Play in browser @@ -634,7 +635,9 @@ VLC MPV Web Video Cast + Fcast Web browser + Select cast device App not found All Languages Skip %s From af828de8d5264e7d2c3a6d6954b0a2a228ca2264 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 18 May 2024 14:41:37 +0300 Subject: [PATCH 066/157] feat(TV UI: Fix online subtitles dialog focus (#1085) --- app/src/main/res/layout/dialog_online_subtitles.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml index 7803e261..d480bd34 100644 --- a/app/src/main/res/layout/dialog_online_subtitles.xml +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -40,7 +40,7 @@ android:layout_width="match_parent" android:layout_height="30dp" android:layout_gravity="center_vertical" - android:layout_marginEnd="30dp"> + android:layout_marginEnd="40dp"> @@ -106,7 +107,7 @@ android:layout_margin="10dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/change_providers_img_des" - android:nextFocusLeft="@id/main_search" + android:nextFocusLeft="@id/year_btt" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" android:nextFocusDown="@id/search_autofit_results" From 4d5cd288abd07c74fc88900c58e119c27f1b7867 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sat, 18 May 2024 11:47:12 +0000 Subject: [PATCH 067/157] Ported more files for multiplatform (#1056) --- .../lagradost/cloudstream3/CommonActivity.kt | 1 + .../com/lagradost/cloudstream3/MainAPI.kt | 114 ++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 197 ++++++++---------- .../ui/result/ResultViewModel2.kt | 10 +- .../cloudstream3/utils/Coroutines.android.kt | 11 + .../lagradost/cloudstream3/MainActivity.kt | 35 ++++ .../com/lagradost/cloudstream3/MainApi.kt | 3 + .../lagradost/cloudstream3/ParCollections.kt | 0 .../cloudstream3/utils/Coroutines.kt | 8 +- .../lagradost/cloudstream3/utils/JsHunter.kt | 0 .../cloudstream3/utils/JsUnpacker.kt | 0 .../cloudstream3/utils/Coroutines.jvm.kt | 5 + 12 files changed, 243 insertions(+), 141 deletions(-) create mode 100644 library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/ParCollections.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/Coroutines.kt (90%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/JsHunter.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/JsUnpacker.kt (100%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 4dc78dc7..82e985db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -29,6 +29,7 @@ import com.google.android.material.chip.ChipGroup import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.PlayerEventType diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 699159b5..07a82583 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -31,19 +31,16 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.math.absoluteValue -const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - -//val baseHeader = mapOf("User-Agent" to USER_AGENT) -val mapper = JsonMapper.builder().addModule(kotlinModule()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! - /** * Defines the constant for the all languages preference, if this is set then it is * the equivalent of all languages being set **/ const val AllLanguagesName = "universal" +//val baseHeader = mapOf("User-Agent" to USER_AGENT) +val mapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! + object APIHolder { val unixTime: Long get() = System.currentTimeMillis() / 1000L @@ -121,7 +118,8 @@ object APIHolder { fun LoadResponse.getId(): Int { // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked - return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) ?: getLoadResponseIdFromUrl(url, apiName) + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) + ?: getLoadResponseIdFromUrl(url, apiName) } /** @@ -222,10 +220,15 @@ object APIHolder { } ?: false val matchingTypes = types?.any { it.name.equals(media.format, true) } == true - if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears + if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears } ?: return null - Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) + Tracker( + res.idMal, + res.id.toString(), + res.coverImage?.extraLarge ?: res.coverImage?.large, + res.bannerImage + ) } catch (t: Throwable) { logError(t) null @@ -866,6 +869,7 @@ enum class TvType(value: Int?) { Others(12), Music(13), AudioBook(14), + /** Wont load the built in player, make your own interaction */ CustomMedia(15), } @@ -1253,13 +1257,15 @@ interface LoadResponse { fun LoadResponse.getImdbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb) + 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) + SimklApi.readIdFromString(this.syncData[simklIdPrefix]) + ?.get(SimklApi.Companion.SyncServices.Tmdb) } } @@ -1556,8 +1562,26 @@ data class TorrentLoadResponse( posterHeaders: Map? = null, backgroundPosterUrl: String? = null, ) : this( - name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null + name, + url, + apiName, + magnet, + torrent, + plot, + type, + posterUrl, + year, + rating, + tags, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + backgroundPosterUrl, + null ) } @@ -1609,7 +1633,8 @@ data class AnimeLoadResponse( return this.episodes.maxOf { (_, episodes) -> episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = + displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags, - synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders, - nextAiring, seasonNames, backgroundPosterUrl, null + engName, + japName, + name, + url, + apiName, + type, + posterUrl, + year, + episodes, + showStatus, + plot, + tags, + synonyms, + rating, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + nextAiring, + seasonNames, + backgroundPosterUrl, + null ) } @@ -1780,7 +1827,7 @@ data class MovieLoadResponse( backgroundPosterUrl: String? = null, ) : this( name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl,null + recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null ) } @@ -1923,7 +1970,8 @@ data class TvSeriesLoadResponse( return episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = + displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration, - trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames, - backgroundPosterUrl, null + name, + url, + apiName, + type, + episodes, + posterUrl, + year, + plot, + showStatus, + rating, + tags, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + nextAiring, + seasonNames, + backgroundPosterUrl, + null ) } @@ -2022,6 +2089,7 @@ data class AniSearch( @JsonProperty("extraLarge") var extraLarge: String? = null, @JsonProperty("large") var large: String? = null, ) + data class Title( @JsonProperty("romaji") var romaji: String? = null, @JsonProperty("english") var english: String? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 56322b73..1ff0575b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -174,7 +174,6 @@ import java.net.URLDecoder import java.nio.charset.Charset import kotlin.math.abs import kotlin.math.absoluteValue -import kotlin.reflect.KClass import kotlin.system.exitProcess //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 @@ -187,117 +186,93 @@ import kotlin.system.exitProcess //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 -const val VLC_PACKAGE = "org.videolan.vlc" -const val MPV_PACKAGE = "is.xyz.mpv" -const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" - -val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") -val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") - -//TODO REFACTOR AF -open class ResultResume( - val packageString: String, - val action: String = Intent.ACTION_VIEW, - val position: String? = null, - val duration: String? = null, - var launcher: ActivityResultLauncher? = null, -) { - val defaultTime = -1L - - val lastId get() = "${packageString}_last_open_id" - suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { - val intent = Intent(action) - - if (id != null) - setKey(lastId, id) - else - removeKey(lastId) - - intent.setPackage(packageString) - callback.invoke(intent) - launcher?.launch(intent) - } - - open fun getPosition(intent: Intent?): Long { - return defaultTime - } - - open fun getDuration(intent: Intent?): Long { - return defaultTime - } -} - -val VLC = object : ResultResume( - VLC_PACKAGE, - // Android 13 intent restrictions fucks up specifically launching the VLC player - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - "org.videolan.vlc.player.result" - } else { - Intent.ACTION_VIEW - }, - "extra_position", - "extra_duration", -) { - override fun getPosition(intent: Intent?): Long { - return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime - } -} - -val MPV = object : ResultResume( - MPV_PACKAGE, - //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: - position = "position", - duration = "duration", -) { - override fun getPosition(intent: Intent?): Long { - return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime - } -} - -val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) - -val resumeApps = arrayOf( - VLC, MPV, WEB_VIDEO -) - -// Short name for requests client to make it nicer to use - -var app = Requests(responseParser = object : ResponseParser { - val mapper: ObjectMapper = jacksonObjectMapper().configure( - DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, - false - ) - - override fun parse(text: String, kClass: KClass): T { - return mapper.readValue(text, kClass.java) - } - - override fun parseSafe(text: String, kClass: KClass): T? { - return try { - mapper.readValue(text, kClass.java) - } catch (e: Exception) { - null - } - } - - override fun writeValueAsString(obj: Any): String { - return mapper.writeValueAsString(obj) - } -}).apply { - defaultHeaders = mapOf("user-agent" to USER_AGENT) -} - class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback { companion object { + const val VLC_PACKAGE = "org.videolan.vlc" + const val MPV_PACKAGE = "is.xyz.mpv" + const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" + + val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") + val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") + + //TODO REFACTOR AF + open class ResultResume( + val packageString: String, + val action: String = Intent.ACTION_VIEW, + val position: String? = null, + val duration: String? = null, + var launcher: ActivityResultLauncher? = null, + ) { + val defaultTime = -1L + + val lastId get() = "${packageString}_last_open_id" + suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { + val intent = Intent(action) + + if (id != null) + setKey(lastId, id) + else + removeKey(lastId) + + intent.setPackage(packageString) + callback.invoke(intent) + launcher?.launch(intent) + } + + open fun getPosition(intent: Intent?): Long { + return defaultTime + } + + open fun getDuration(intent: Intent?): Long { + return defaultTime + } + } + + val VLC = object : ResultResume( + VLC_PACKAGE, + // Android 13 intent restrictions fucks up specifically launching the VLC player + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + "org.videolan.vlc.player.result" + } else { + Intent.ACTION_VIEW + }, + "extra_position", + "extra_duration", + ) { + override fun getPosition(intent: Intent?): Long { + return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime + } + } + + val MPV = object : ResultResume( + MPV_PACKAGE, + //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: + position = "position", + duration = "duration", + ) { + override fun getPosition(intent: Intent?): Long { + return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() + ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() + ?: defaultTime + } + } + + val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) + + val resumeApps = arrayOf( + VLC, MPV, WEB_VIDEO + ) + + const val TAG = "MAINACT" const val ANIMATED_OUTLINE: Boolean = false var lastError: String? = null @@ -1403,7 +1378,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } } - observe(viewModel.watchStatus,::setWatchStatus) + observe(viewModel.watchStatus, ::setWatchStatus) observe(syncViewModel.userData, ::setUserData) observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus) @@ -1831,7 +1806,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } override fun onAuthenticationError() { - finish() + finish() } private var backPressedCallback: OnBackPressedCallback? = null 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 a32942f6..0af01ca8 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 @@ -29,6 +29,14 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie +import com.lagradost.cloudstream3.MainActivity.Companion.MPV +import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT +import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE +import com.lagradost.cloudstream3.MainActivity.Companion.VLC +import com.lagradost.cloudstream3.MainActivity.Companion.VLC_COMPONENT +import com.lagradost.cloudstream3.MainActivity.Companion.VLC_PACKAGE +import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO +import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO_CAST_PACKAGE import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager @@ -1354,7 +1362,7 @@ class ResultViewModel2 : ViewModel() { private fun launchActivity( activity: Activity?, - resumeApp: ResultResume, + resumeApp: MainActivity.Companion.ResultResume, id: Int? = null, work: suspend (Intent.(Activity) -> Unit) ): Job? { diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt new file mode 100644 index 00000000..48a709eb --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt @@ -0,0 +1,11 @@ +package com.lagradost.cloudstream3.utils + +import android.os.Handler +import android.os.Looper + +actual fun runOnMainThreadNative(work: () -> Unit) { + val mainHandler = Handler(Looper.getMainLooper()) + mainHandler.post { + work() + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt new file mode 100644 index 00000000..6502cc83 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt @@ -0,0 +1,35 @@ +package com.lagradost.cloudstream3 + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.lagradost.nicehttp.Requests +import com.lagradost.nicehttp.ResponseParser +import kotlin.reflect.KClass + +// Short name for requests client to make it nicer to use + +var app = Requests(responseParser = object : ResponseParser { + val mapper: ObjectMapper = jacksonObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false + ) + + override fun parse(text: String, kClass: KClass): T { + return mapper.readValue(text, kClass.java) + } + + override fun parseSafe(text: String, kClass: KClass): T? { + return try { + mapper.readValue(text, kClass.java) + } catch (e: Exception) { + null + } + } + + override fun writeValueAsString(obj: Any): String { + return mapper.writeValueAsString(obj) + } +}).apply { + defaultHeaders = mapOf("user-agent" to USER_AGENT) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt index 87ee4815..160ff098 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -1,3 +1,6 @@ package com.lagradost.cloudstream3 +const val USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt similarity index 90% rename from app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt index c3b244c2..f87ddc6a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -1,12 +1,11 @@ package com.lagradost.cloudstream3.utils -import android.os.Handler -import android.os.Looper import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import java.util.Collections.synchronizedList +expect fun runOnMainThreadNative(work: (() -> Unit)) object Coroutines { fun T.main(work: suspend ((T) -> Unit)): Job { val value = this @@ -50,10 +49,7 @@ object Coroutines { } fun runOnMainThread(work: (() -> Unit)) { - val mainHandler = Handler(Looper.getMainLooper()) - mainHandler.post { - work() - } + runOnMainThreadNative(work) } /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt new file mode 100644 index 00000000..0a9667cb --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt @@ -0,0 +1,5 @@ +package com.lagradost.cloudstream3.utils + +actual fun runOnMainThreadNative(work: () -> Unit) { + work.invoke() +} \ No newline at end of file From 469a71236b6e78018f3f72c83f0b051bdab189c3 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 18 May 2024 19:15:23 +0300 Subject: [PATCH 068/157] SubDL subtitles provider (#1082) --- .../subtitles/AbstractSubtitleEntities.kt | 6 +- .../syncproviders/AccountManager.kt | 5 +- .../providers/IndexSubtitleApi.kt | 2 +- .../providers/OpenSubtitlesApi.kt | 2 +- .../syncproviders/providers/Subdl.kt | 102 ++++++++++++++++++ .../ui/player/FullScreenPlayer.kt | 3 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 18 +++- .../ui/player/PlayerGeneratorViewModel.kt | 4 + .../ui/result/ResultTrailerPlayer.kt | 3 +- .../ui/result/ResultViewModel2.kt | 3 +- 10 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt index f6424c4c..ed4ccb74 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.subtitles +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.TvType class AbstractSubtitleEntities { @@ -19,8 +20,11 @@ class AbstractSubtitleEntities { data class SubtitleSearch( var query: String = "", - var imdb: Long? = null, var lang: String? = null, + var imdbId: String? = null, + var tmdbId: Int? = null, + var malId: Int? = null, + var aniListId: Int? = null, var epNumber: Int? = null, var seasonNumber: Int? = null, var year: Int? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index bae8a5df..55418890 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.syncproviders.providers.SubScene import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit @@ -16,6 +15,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val subScene = SubScene() + val subDl = SubDL() val localListApi = LocalList() // used to login via app intent @@ -44,7 +44,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subScene + subScene, + subDl ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt index 1adecce9..5ca3f3d5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt @@ -98,7 +98,7 @@ class IndexSubtitleApi : AbstractSubApi { } override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List { - val imdbId = query.imdb ?: 0 + val imdbId = query.imdbId?.replace("tt", "")?.toLong() ?: 0 val lang = query.lang val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) val queryText = query.query diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 4030649d..7d0514d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val fixedLang = fixLanguage(query.lang) - val imdbId = query.imdb ?: 0 + val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0 val queryText = query.query val epNum = query.epNumber ?: 0 val seasonNum = query.seasonNumber ?: 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt new file mode 100644 index 00000000..d25d3f22 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -0,0 +1,102 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource + +class SubDL : AbstractSubProvider { + //API Documentation: https://subdl.com/api-doc + val mainUrl = "https://subdl.com/" + val name = "SubDL" + override val idPrefix = "subdl" + companion object { + const val APIKEY = "zRJl5QA-8jNA2i0pE8cxANbEukANp7IM" + const val APIENDPOINT = "https://api.subdl.com/api/v1/subtitles" + const val DOWNLOADENDPOINT = "https://dl.subdl.com" + } + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + + val queryText = query.query + val epNum = query.epNumber ?: 0 + val seasonNum = query.seasonNumber ?: 0 + val yearNum = query.year ?: 0 + + val idQuery = when { + query.imdbId != null -> "&imdb_id=${query.imdbId}" + query.tmdbId != null -> "&tmdb_id=${query.tmdbId}" + else -> null + } + + val epQuery = if (epNum > 0) "&episode_number=$epNum" else "" + val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else "" + val yearQuery = if (yearNum > 0) "&year=$yearNum" else "" + + val searchQueryUrl = when (idQuery) { + //Use imdb/tmdb id to search if its valid + null -> "$APIENDPOINT?api_key=$APIKEY&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + else -> "$APIENDPOINT?api_key=$APIKEY$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + } + + val req = app.get( + url = searchQueryUrl, + headers = mapOf( + "Accept" to "application/json" + ) + ) + + return req.parsedSafe()?.subtitles?.map { subtitle -> + val name = subtitle.releaseName + val lang = subtitle.lang.replaceFirstChar { it.uppercase() } + val resEpNum = subtitle.episode ?: query.epNumber + val resSeasonNum = subtitle.season ?: query.seasonNumber + val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie + + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = this.idPrefix, + name = name, + lang = lang, + data = "${DOWNLOADENDPOINT}${subtitle.url}", + type = type, + source = this.name, + epNumber = resEpNum, + seasonNumber = resSeasonNum, + ) + } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + this.addZipUrl(data.data) { name, _ -> + name + } + } + + data class ApiResponse( + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("results") val results: List? = null, + @JsonProperty("subtitles") val subtitles: List? = null, + ) + + data class Result( + @JsonProperty("sd_id") val sdId: Int? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("tmdb_id") val tmdbId: Long? = null, + @JsonProperty("first_air_date") val firstAirDate: String? = null, + @JsonProperty("year") val year: Int? = null, + ) + + data class Subtitle( + @JsonProperty("release_name") val releaseName: String, + @JsonProperty("name") val name: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("author") val author: String? = null, + @JsonProperty("url") val url: String? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, + ) +} \ 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 c357ce9c..aa25157b 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 @@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding @@ -177,7 +178,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { open fun openOnlineSubPicker( context: Context, - imdbId: Long?, + loadResponse: LoadResponse?, dismissCallback: (() -> Unit) ) { throw NotImplementedError() 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 7ff56886..c77f9404 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 @@ -25,6 +25,10 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding @@ -39,7 +43,6 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -258,6 +261,7 @@ class GeneratorPlayer : FullScreenPlayer() { var episode: Int? = null, var season: Int? = null, var name: String? = null, + var imdbId: String? = null, ) private fun getMetaData(): TempMetaData { @@ -284,7 +288,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun openOnlineSubPicker( - context: Context, imdbId: Long?, dismissCallback: (() -> Unit) + context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit) ) { val providers = subsProviders val isSingleProvider = subsProviders.size == 1 @@ -377,6 +381,7 @@ class GeneratorPlayer : FullScreenPlayer() { } val currentTempMeta = getMetaData() + // bruh idk why it is not correct val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) binding.searchLoadingBar.progressTintList = color @@ -424,7 +429,10 @@ class GeneratorPlayer : FullScreenPlayer() { val search = AbstractSubtitleEntities.SubtitleSearch( query = query ?: return@ioSafe, - imdb = imdbId, + imdbId = loadResponse?.getImdbId(), + tmdbId = loadResponse?.getTMDbId()?.toInt(), + malId = loadResponse?.getMalId()?.toInt(), + aniListId = loadResponse?.getAniListId()?.toInt(), epNumber = currentTempMeta.episode, seasonNumber = currentTempMeta.season, lang = currentLanguageTwoLetters.ifBlank { null }, @@ -633,6 +641,8 @@ class GeneratorPlayer : FullScreenPlayer() { } if (subsProvidersIsActive) { + val currentLoadResponse = viewModel.getLoadResponse() + val loadFromOpenSubsFooter: TextView = layoutInflater.inflate( R.layout.sort_bottom_footer_add_choice, null ) as TextView @@ -643,7 +653,7 @@ class GeneratorPlayer : FullScreenPlayer() { loadFromOpenSubsFooter.setOnClickListener { shouldDismiss = false sourceDialog.dismissSafe(activity) - openOnlineSubPicker(it.context, null) { + openOnlineSubPicker(it.context, currentLoadResponse) { dismiss() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 0d98f205..ee44567f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError @@ -111,6 +112,9 @@ class PlayerGeneratorViewModel : ViewModel() { } } } + fun getLoadResponse(): LoadResponse? { + return normalSafeApiCall { (generator as? RepoLinkGenerator?)?.page } + } fun getMeta(): Any? { return normalSafeApiCall { generator?.getCurrent() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index ef3db0b4..135dc530 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -12,6 +12,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource @@ -110,7 +111,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { override fun openOnlineSubPicker( context: Context, - imdbId: Long?, + loadResponse: LoadResponse?, dismissCallback: () -> Unit ) { } 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 0af01ca8..e1a52074 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 @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.MainActivity.Companion.MPV @@ -2417,7 +2418,7 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - null + null, ) ) } From db2bf5e7be3f952e440e02989a0c0878c4bc4b15 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 May 2024 18:43:46 +0800 Subject: [PATCH 069/157] Remove subscene (#1096) subscene.com just shows a "Subscene is closed" message now. --- .../syncproviders/AccountManager.kt | 2 - .../syncproviders/providers/SubScene.kt | 118 ------------------ 2 files changed, 120 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 55418890..e96499f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -14,7 +14,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() - val subScene = SubScene() val subDl = SubDL() val localListApi = LocalList() @@ -44,7 +43,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subScene, subDl ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt deleted file mode 100644 index fbe05026..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.debugPrint -import com.lagradost.cloudstream3.subtitles.AbstractSubProvider -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class SubScene : AbstractSubProvider { - val mainUrl = "https://subscene.com" - val name = "Subscene" - override val idPrefix = "subscene" - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { - val seasonName = - query.seasonNumber?.let { number -> - // Need to translate "7" to "Seventh Season" - getOrdinal(number)?.let { words -> " - $words Season" } - } ?: "" - - val fullQuery = query.query + seasonName - - val doc = app.post( - "$mainUrl/subtitles/searchbytitle", - data = mapOf("query" to fullQuery, "l" to "") - ).document - - return doc.select("div.title a").map { element -> - val href = "$mainUrl${element.attr("href")}" - val title = element.text() - - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = idPrefix, - name = title, - source = name, - data = href, - lang = query.lang ?: "en", - epNumber = query.epNumber - ) - }.distinctBy { it.data } - } - - override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { - val resultDoc = app.get(data.data).document - val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English" - - val results = resultDoc.select("table tbody tr").mapNotNull { element -> - val anchor = element.select("a") - val href = anchor.attr("href") ?: return@mapNotNull null - val fixedHref = "$mainUrl${href}" - val spans = anchor.select("span") - val language = spans.firstOrNull()?.text() - val title = spans.getOrNull(1)?.text() - val isPositive = anchor.select("span.positive-icon").isNotEmpty() - - TableElement(title, language, fixedHref, isPositive) - }.sortedBy { - it.getScore(queryLanguage, data.epNumber) - } - - debugPrint { "$name found subtitles: ${results.takeLast(3)}" } - // Last = highest score - val selectedResult = results.lastOrNull() ?: return - - val subtitleDocument = app.get(selectedResult.href).document - val subtitleDownloadUrl = - "$mainUrl${subtitleDocument.select("div.download a").attr("href")}" - - this.addZipUrl(subtitleDownloadUrl) { name, _ -> - name - } - } - - /** - * Class to manage the various different subtitle results and rank them. - */ - data class TableElement( - val title: String?, - val language: String?, - val href: String, - val isPositive: Boolean - ) { - private fun matchesLanguage(other: String): Boolean { - return language != null && (language.contains(other, ignoreCase = true) || - other.contains(language, ignoreCase = true)) - } - - /** - * Scores in this order: - * Preferred Language > Episode number > Positive rating > English Language - */ - fun getScore(queryLanguage: String, episodeNum: Int?): Int { - var score = 0 - if (this.matchesLanguage(queryLanguage)) { - score += 8 - } - // Matches Episode 7 using "E07" with any number of leading zeroes - if (episodeNum != null && title != null && title.contains( - Regex( - """E0*${episodeNum}""", - RegexOption.IGNORE_CASE - ) - ) - ) { - score += 4 - } - if (isPositive) { - score += 2 - } - if (this.matchesLanguage("English")) { - score += 1 - } - return score - } - } -} \ No newline at end of file From e697bf75544d0777e21f67c5af3bef8392bbd35b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 21 May 2024 23:06:28 +0300 Subject: [PATCH 070/157] Next Airing episode support in Trakt meta provider (#1072) --- .../cloudstream3/metaproviders/TraktProvider.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 37c6be1b..07c9f316 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.lagradost.cloudstream3.* import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer @@ -166,6 +167,7 @@ open class TraktProvider : MainAPI() { val episodes = mutableListOf() val seasons = parseJson>(resSeasons) val seasonsNames = mutableListOf() + var nextAir: NextAiring? = null seasons.forEach { season -> @@ -215,6 +217,13 @@ open class TraktProvider : MainAPI() { description = episode.overview, ).apply { this.addDate(episode.firstAired) + if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { + nextAir = NextAiring( + episode = this.episode!!, + unixTime = this.date!!.div(1000L), + season = if (this.season == 1) null else this.season, + ) + } } ) } @@ -240,6 +249,7 @@ open class TraktProvider : MainAPI() { this.actors = actors this.comingSoon = isUpcoming(mediaDetails.released) //posterHeaders + this.nextAiring = nextAir this.seasonNames = seasonsNames this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) this.contentRating = mediaDetails.certification From d0852449a50f548f88cd72785c338f2a5ad45184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 27 May 2024 16:54:25 +0300 Subject: [PATCH 071/157] Extractor: Added FourPichive (#1103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕊 --- .../cloudstream3/extractors/HotlingerExtractor.kt | 7 ++++++- .../java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index b557a53e..db721108 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -20,4 +20,9 @@ class PlayRu : ContentX() { class FourPlayRu : ContentX() { override var name = "FourPlayRu" override var mainUrl = "https://four.playru.net" -} \ No newline at end of file +} + +class FourPichive : ContentX() { + override var name = "FourPichive" + override var mainUrl = "https://four.pichive.online" +} 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 61cdd26a..c6cad804 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -110,6 +110,7 @@ import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.PlayRu import com.lagradost.cloudstream3.extractors.FourPlayRu +import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.VideoSeyred @@ -748,6 +749,7 @@ val extractorApis: MutableList = arrayListOf( FourCX(), PlayRu(), FourPlayRu(), + FourPichive(), HDMomPlayer(), HDPlayerSystem(), VideoSeyred(), From 960f8449b7eda687f70b9cebbbfd76502cffa398 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 27 May 2024 13:54:51 +0000 Subject: [PATCH 072/157] Update ResultViewModel2.kt (#1102) --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e1a52074..4285feb1 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 @@ -1728,7 +1728,7 @@ class ResultViewModel2 : ViewModel() { txt(R.string.episode_action_cast_mirror) ) { (result, index) -> val host = device?.host ?: return@acquireSingleLink - val link = result.links.firstOrNull() ?: return@acquireSingleLink + val link = result.links.getOrNull(index) ?: return@acquireSingleLink FcastSession(host).use { session -> session.sendMessage( From 5502e478c4f9227e6da2f785b436e9c599d06fdf Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 27 May 2024 19:35:56 +0530 Subject: [PATCH 073/157] chore: update material,kotlin compiler,newpipe extractor,rhino-js,guava,corektx (#1091) --- app/build.gradle.kts | 12 +++++------- .../java/com/lagradost/cloudstream3/MainActivity.kt | 2 +- .../ui/player/DownloadedPlayerActivity.kt | 2 +- build.gradle.kts | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f854865d..61a0634f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -164,7 +164,7 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") // Android Core & Lifecycle - implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") @@ -174,7 +174,7 @@ dependencies { // Design & UI implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.12.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") @@ -185,7 +185,7 @@ dependencies { // For KSP -> Official Annotation Processors are Not Yet Supported for KSP ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:32.1.3-android") + implementation("com.google.guava:guava:33.2.0-android") implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") // Media 3 (ExoPlayer) @@ -202,7 +202,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding @@ -219,9 +219,7 @@ dependencies { implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview // Extensions & Other Libs - implementation("org.mozilla:rhino:1.7.13") /* run JavaScript - ^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring) - NewPipeExtractor Issue */ + implementation("org.mozilla:rhino:1.7.15") // run JavaScript implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 1ff0575b..cc2c99de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -652,7 +652,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } } - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { val response = CommonActivity.dispatchKeyEvent(this, event) if (response != null) return response 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 1e2ea540..4d8860f8 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 @@ -17,7 +17,7 @@ import com.lagradost.safefile.SafeFile const val DTAG = "PlayerActivity" class DownloadedPlayerActivity : AppCompatActivity() { - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { CommonActivity.dispatchKeyEvent(this, event)?.let { return it } diff --git a/build.gradle.kts b/build.gradle.kts index ab1918fe..34f141b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:8.2.2") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1") From dff56026de873d0f35cdd134decd1fa1008c0f5f Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 29 May 2024 23:39:55 +0300 Subject: [PATCH 074/157] SubDL Account login support (#1101) --- .../syncproviders/AccountManager.kt | 11 +- .../syncproviders/providers/Subdl.kt | 167 ++++++++++++++++-- .../ui/settings/SettingsAccount.kt | 2 + .../cloudstream3/utils/BackupUtils.kt | 2 + app/src/main/res/drawable/subdl_logo_big.xml | 10 ++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_account.xml | 4 + 7 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 app/src/main/res/drawable/subdl_logo_big.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index e96499f0..1fd7900f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -14,7 +14,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() - val subDl = SubDL() + val subDlApi = SubDlApi(0) val localListApi = LocalList() // used to login via app intent @@ -26,7 +26,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // this needs init with context and can be accessed in settings val accountManagers get() = listOf( - malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi + malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi ) // used for active syncing @@ -36,14 +36,17 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { ) val inAppAuths - get() = listOf(openSubtitlesApi)//, nginxApi) + get() = listOf( + openSubtitlesApi, + subDlApi + )//, nginxApi) val subtitleProviders get() = listOf( openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subDl + subDlApi ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt index d25d3f22..29544e65 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -1,21 +1,80 @@ package com.lagradost.cloudstream3.syncproviders.providers import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager -class SubDL : AbstractSubProvider { - //API Documentation: https://subdl.com/api-doc - val mainUrl = "https://subdl.com/" - val name = "SubDL" +class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { override val idPrefix = "subdl" + override val name = "SubDL" + override val icon = R.drawable.subdl_logo_big + override val requiresPassword = true + override val requiresEmail = true + override val createAccountUrl = "https://subdl.com/login" + companion object { - const val APIKEY = "zRJl5QA-8jNA2i0pE8cxANbEukANp7IM" - const val APIENDPOINT = "https://api.subdl.com/api/v1/subtitles" + const val APIURL = "https://api.subdl.com" + const val APIENDPOINT = "$APIURL/api/v1/subtitles" const val DOWNLOADENDPOINT = "https://dl.subdl.com" + const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user" + var currentSession: SubtitleOAuthEntity? = null + } + + override suspend fun initialize() { + currentSession = getAuthKey() + } + + override fun logOut() { + setAuthKey(null) + removeAccountKeys() + currentSession = getAuthKey() + } + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + val email = data.email ?: throw ErrorLoadingException("Requires Email") + val password = data.password ?: throw ErrorLoadingException("Requires Password") + switchToNewAccount() + try { + if (initLogin(email, password)) { + registerAccount() + return true + } + } catch (e: Exception) { + logError(e) + switchToOldAccount() + } + switchToOldAccount() + return false + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + val current = getAuthKey() ?: return null + return InAppAuthAPI.LoginData( + email = current.userEmail, + password = current.pass + ) + } + + override fun loginInfo(): LoginInfo? { + getAuthKey()?.let { user -> + return LoginInfo( + profilePicture = null, + name = user.name ?: user.userEmail, + accountIndex = accountIndex + ) + } + return null } override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { @@ -37,8 +96,8 @@ class SubDL : AbstractSubProvider { val searchQueryUrl = when (idQuery) { //Use imdb/tmdb id to search if its valid - null -> "$APIENDPOINT?api_key=$APIKEY&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" - else -> "$APIENDPOINT?api_key=$APIKEY$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" } val req = app.get( @@ -49,7 +108,7 @@ class SubDL : AbstractSubProvider { ) return req.parsedSafe()?.subtitles?.map { subtitle -> - val name = subtitle.releaseName + val lang = subtitle.lang.replaceFirstChar { it.uppercase() } val resEpNum = subtitle.episode ?: query.epNumber val resSeasonNum = subtitle.season ?: query.seasonNumber @@ -57,13 +116,14 @@ class SubDL : AbstractSubProvider { AbstractSubtitleEntities.SubtitleEntity( idPrefix = this.idPrefix, - name = name, + name = subtitle.releaseName, lang = lang, data = "${DOWNLOADENDPOINT}${subtitle.url}", type = type, source = this.name, epNumber = resEpNum, seasonNumber = resSeasonNum, + isHearingImpaired = subtitle.hearingImpaired ?: false, ) } } @@ -74,6 +134,88 @@ class SubDL : AbstractSubProvider { } } + private suspend fun initLogin(useremail: String, password: String): Boolean { + + val tokenResponse = app.post( + url = "$APIURL/login", + data = mapOf( + "email" to useremail, + "password" to password + ) + ).parsedSafe() + + if (tokenResponse?.token == null) return false + + val apiResponse = app.get( + url = "$APIURL/user/userApi", + headers = mapOf( + "Authorization" to "Bearer ${tokenResponse.token}" + ) + ).parsedSafe() + + if (apiResponse?.ok == false) return false + + setAuthKey( + SubtitleOAuthEntity( + userEmail = useremail, + pass = password, + name = tokenResponse.userData?.username ?: tokenResponse.userData?.name, + accessToken = tokenResponse.token, + apiKey = apiResponse?.apiKey + ) + ) + return true + } + + private fun getAuthKey(): SubtitleOAuthEntity? { + return getKey(accountId, SUBDL_SUBTITLES_USER_KEY) + } + + private fun setAuthKey(data: SubtitleOAuthEntity?) { + if (data == null) removeKey( + accountId, + SUBDL_SUBTITLES_USER_KEY + ) + currentSession = data + setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data) + } + + data class SubtitleOAuthEntity( + @JsonProperty("userEmail") var userEmail: String, + @JsonProperty("pass") var pass: String, + @JsonProperty("name") var name: String? = null, + @JsonProperty("accessToken") var accessToken: String? = null, + @JsonProperty("apiKey") var apiKey: String? = null, + ) + + data class OAuthTokenResponse( + @JsonProperty("token") val token: String? = null, + @JsonProperty("userData") val userData: UserData? = null, + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("message") val message: String? = null, + ) + + data class UserData( + @JsonProperty("email") val email: String, + @JsonProperty("name") val name: String, + @JsonProperty("country") val country: String, + @JsonProperty("scStepCode") val scStepCode: String, + @JsonProperty("scVerified") val scVerified: Boolean, + @JsonProperty("username") val username: String? = null, + @JsonProperty("scUsername") val scUsername: String, + ) + + data class ApiKeyResponse( + @JsonProperty("ok") val ok: Boolean? = false, + @JsonProperty("api_key") val apiKey: String? = null, + @JsonProperty("usage") val usage: Usage? = null, + ) + + data class Usage( + @JsonProperty("total") val total: Long? = 0, + @JsonProperty("today") val today: Long? = 0, + ) + data class ApiResponse( @JsonProperty("status") val status: Boolean? = null, @JsonProperty("results") val results: List? = null, @@ -96,7 +238,10 @@ class SubDL : AbstractSubProvider { @JsonProperty("lang") val lang: String, @JsonProperty("author") val author: String? = null, @JsonProperty("url") val url: String? = null, + @JsonProperty("subtitlePage") val subtitlePage: String? = null, @JsonProperty("season") val season: Int? = null, @JsonProperty("episode") val episode: Int? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("hi") val hearingImpaired: Boolean? = null, ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index f0d402da..27233525 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniList import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API @@ -324,6 +325,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome R.string.anilist_key to aniListApi, R.string.simkl_key to simklApi, R.string.opensubtitles_key to openSubtitlesApi, + R.string.subdl_key to subDlApi, ) for ((key, api) in syncApis) { 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 279a0cb5..1d23e503 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY +import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi.Companion.SUBDL_SUBTITLES_USER_KEY import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -64,6 +65,7 @@ object BackupUtils { PLUGINS_KEY_LOCAL, OPEN_SUBTITLES_USER_KEY, + SUBDL_SUBTITLES_USER_KEY, DOWNLOAD_EPISODE_CACHE, diff --git a/app/src/main/res/drawable/subdl_logo_big.xml b/app/src/main/res/drawable/subdl_logo_big.xml new file mode 100644 index 00000000..a6cbb311 --- /dev/null +++ b/app/src/main/res/drawable/subdl_logo_big.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8108623..44171dc5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -471,6 +471,7 @@ simkl_key mal_key opensubtitles_key + subdl_key nginx_key password123 Username diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index 5cde06c4..d1d18a0f 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -17,6 +17,10 @@ android:icon="@drawable/open_subtitles_icon" android:key="@string/opensubtitles_key" /> + + Date: Sat, 1 Jun 2024 19:16:42 +0300 Subject: [PATCH 075/157] feat(TV UI): Account switch focus fix (#1112) --- app/src/main/res/layout/account_managment.xml | 14 +++++---- app/src/main/res/layout/account_single.xml | 29 ++++++++++--------- app/src/main/res/layout/account_switch.xml | 22 +++++++------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml index 389a3406..e7afb382 100644 --- a/app/src/main/res/layout/account_managment.xml +++ b/app/src/main/res/layout/account_managment.xml @@ -62,14 +62,16 @@ + android:id="@+id/account_switch_account" + android:text="@string/switch_account" + style="@style/SettingsItem" + android:focusable="true"/> + android:id="@+id/account_logout" + android:text="@string/logout" + style="@style/SettingsItem" + android:focusable="true"> diff --git a/app/src/main/res/layout/account_single.xml b/app/src/main/res/layout/account_single.xml index cbfb9f18..c4f7fa39 100644 --- a/app/src/main/res/layout/account_single.xml +++ b/app/src/main/res/layout/account_single.xml @@ -1,10 +1,11 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:orientation="horizontal" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:focusable="true"> + android:id="@+id/account_profile_picture" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="ContentDescription" /> + android:foreground="@null" + android:id="@+id/account_name" + tools:text="Account 1" + style="@style/SettingsItem" /> diff --git a/app/src/main/res/layout/account_switch.xml b/app/src/main/res/layout/account_switch.xml index 659ad840..5153f0e3 100644 --- a/app/src/main/res/layout/account_switch.xml +++ b/app/src/main/res/layout/account_switch.xml @@ -7,18 +7,20 @@ android:layout_height="match_parent"> + android:id="@+id/account_list" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:background="?attr/primaryBlackBackground" + tools:listitem="@layout/account_single" + android:layout_width="match_parent" + android:layout_rowWeight="1" + android:layout_height="wrap_content" + android:focusable="true"/> + android:id="@+id/account_add" + android:text="@string/add_account" + style="@style/SettingsItem" + android:focusable="true"> From b3e3dadc72e61cd9925f721749578b074026f745 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 1 Jun 2024 19:17:41 +0300 Subject: [PATCH 076/157] Remove IndexSubtitles provider (#1111) --- .../syncproviders/AccountManager.kt | 2 - .../providers/IndexSubtitleApi.kt | 265 ------------------ 2 files changed, 267 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 1fd7900f..a14f8438 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -12,7 +12,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) val simklApi = SimklApi(0) - val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() @@ -44,7 +43,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val subtitleProviders get() = listOf( openSubtitlesApi, - indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, subDlApi ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt deleted file mode 100644 index 5ca3f3d5..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ /dev/null @@ -1,265 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import android.util.Log -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.imdbUrlToIdNullable -import com.lagradost.cloudstream3.subtitles.AbstractSubApi -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class IndexSubtitleApi : AbstractSubApi { - override val name = "IndexSubtitle" - override val idPrefix = "indexsubtitle" - override val requiresLogin = false - override val icon: Nothing? = null - override val createAccountUrl: Nothing? = null - - override fun loginInfo(): Nothing? = null - - override fun logOut() {} - - - companion object { - const val host = "https://indexsubtitle.com" - const val TAG = "INDEXSUBS" - - fun getOrdinal(num: Int?): String? { - return when (num) { - 1 -> "First" - 2 -> "Second" - 3 -> "Third" - 4 -> "Fourth" - 5 -> "Fifth" - 6 -> "Sixth" - 7 -> "Seventh" - 8 -> "Eighth" - 9 -> "Ninth" - 10 -> "Tenth" - 11 -> "Eleventh" - 12 -> "Twelfth" - 13 -> "Thirteenth" - 14 -> "Fourteenth" - 15 -> "Fifteenth" - 16 -> "Sixteenth" - 17 -> "Seventeenth" - 18 -> "Eighteenth" - 19 -> "Nineteenth" - 20 -> "Twentieth" - 21 -> "Twenty-First" - 22 -> "Twenty-Second" - 23 -> "Twenty-Third" - 24 -> "Twenty-Fourth" - 25 -> "Twenty-Fifth" - 26 -> "Twenty-Sixth" - 27 -> "Twenty-Seventh" - 28 -> "Twenty-Eighth" - 29 -> "Twenty-Ninth" - 30 -> "Thirtieth" - 31 -> "Thirty-First" - 32 -> "Thirty-Second" - 33 -> "Thirty-Third" - 34 -> "Thirty-Fourth" - 35 -> "Thirty-Fifth" - else -> null - } - } - } - - private fun fixUrl(url: String): String { - if (url.startsWith("http")) { - return url - } - if (url.isEmpty()) { - return "" - } - - val startsWithNoHttp = url.startsWith("//") - if (startsWithNoHttp) { - return "https:$url" - } else { - if (url.startsWith('/')) { - return host + url - } - return "$host/$url" - } - } - - private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean { - val FILTER_EPS_REGEX = - Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))") - return text.contains(FILTER_EPS_REGEX) - } - - private fun haveEps(text: String): Boolean { - val HAVE_EPS_REGEX = - Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))") - return text.contains(HAVE_EPS_REGEX) - } - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List { - val imdbId = query.imdbId?.replace("tt", "")?.toLong() ?: 0 - val lang = query.lang - val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) - val queryText = query.query - val epNum = query.epNumber ?: 0 - val seasonNum = query.seasonNumber ?: 0 - val yearNum = query.year ?: 0 - - val urlItems = ArrayList() - - fun cleanResources( - results: MutableList, - name: String, - link: String - ) { - results.add( - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = idPrefix, - name = name, - lang = queryLang.toString(), - data = link, - source = this.name, - type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie, - epNumber = epNum, - seasonNumber = seasonNum, - year = yearNum, - ) - ) - } - - val document = app.get("$host/?search=$queryText").document - - document.select("div.my-3.p-3 div.media").map { block -> - if (seasonNum > 0) { - val name = block.select("strong.text-primary, strong.text-info").text().trim() - val season = getOrdinal(seasonNum) - if ((block.selectFirst("a")?.attr("href") - ?.contains( - "$season", - ignoreCase = true - )!! || name.contains( - "$season", - ignoreCase = true - )) && name.contains(queryText, ignoreCase = true) - ) { - block.select("div.media").mapNotNull { - urlItems.add( - fixUrl( - it.selectFirst("a")!!.attr("href") - ) - ) - } - } - } else { - if (block.selectFirst("strong")!!.text().trim() - .matches(Regex("(?i)^$queryText\$")) - ) { - if (block.select("span[title=Release]").isNullOrEmpty()) { - block.select("div.media").mapNotNull { - val urlItem = fixUrl( - it.selectFirst("a")!!.attr("href") - ) - val itemDoc = app.get(urlItem).document - val id = imdbUrlToIdNullable( - itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent() - ?.attr("href") - )?.toLongOrNull() - val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success") - ?.ownText() - ?.trim().toString() - Log.i(TAG, "id => $id \nyear => $year||$yearNum") - if (imdbId > 0) { - if (id == imdbId) { - urlItems.add(urlItem) - } - } else { - if (year.contains("$yearNum")) { - urlItems.add(urlItem) - } - } - } - } else { - if (block.select("span[title=Release]").text().trim() - .contains("$yearNum") - ) { - block.select("div.media").mapNotNull { - urlItems.add( - fixUrl( - it.selectFirst("a")!!.attr("href") - ) - ) - } - } - } - } - } - } - Log.i(TAG, "urlItems => $urlItems") - val results = mutableListOf() - - urlItems.forEach { url -> - val request = app.get(url) - if (request.isSuccessful) { - request.document.select("div.my-3.p-3 div.media").map { block -> - if (block.select("span.d-block span[data-original-title=Language]").text() - .trim() - .contains("$queryLang") - ) { - var name = block.select("strong.text-primary, strong.text-info").text().trim() - val link = fixUrl(block.selectFirst("a")!!.attr("href")) - if (seasonNum > 0) { - when { - isRightEps(name, seasonNum, epNum) -> { - cleanResources(results, name, link) - } - !(haveEps(name)) -> { - name = "$name (S${seasonNum}:E${epNum})" - cleanResources(results, name, link) - } - } - } else { - cleanResources(results, name, link) - } - } - } - } - } - return results - } - - override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? { - val seasonNum = data.seasonNumber - val epNum = data.epNumber - - val req = app.get(data.data) - - if (req.isSuccessful) { - val document = req.document - val link = if (document.select("div.my-3.p-3 div.media").size == 1) { - fixUrl( - document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href") - ) - } else { - document.select("div.my-3.p-3 div.media").firstNotNullOf { block -> - val name = - block.selectFirst("strong.d-block")?.text()?.trim().toString() - if (seasonNum!! > 0) { - if (isRightEps(name, seasonNum, epNum)) { - fixUrl(block.selectFirst("a")!!.attr("href")) - } else { - null - } - } else { - fixUrl(block.selectFirst("a")!!.attr("href")) - } - } - } - return link - } - - return null - - } - -} \ No newline at end of file From 9bebfe459005021970e25bd5ca38816fa5a66ba4 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:07:54 +0200 Subject: [PATCH 077/157] feature(ui): hide NSFW plugins (#1117) Hide NSFW plugins if Settings / Providers NSFW is disabled --- .../cloudstream3/ui/settings/extensions/PluginsViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 151c8d57..2b026e0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.amap @@ -181,8 +182,11 @@ class PluginsViewModel : ViewModel() { } private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) { + val isAdult = settingsForProvider.enableAdult val plugins = getPlugins(repositoryUrl) - val list = plugins.map { plugin -> + val list = plugins.filter { + return@filter !(it.second.tvTypes?.contains("NSFW") == true && !isAdult) + }.map { plugin -> PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) } From 0391a3b89cb3d4266afc1e1a710366b9266f1241 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:09:05 +0200 Subject: [PATCH 078/157] feature(ui): added wikipedia to links (#1119) --- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_general.xml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44171dc5..deee5ad2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -774,4 +774,5 @@ Audio Book Media Reset + CloudStream Wiki \ No newline at end of file diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index cdda6d85..853bbda1 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -86,6 +86,14 @@ android:action="android.intent.action.VIEW" android:data="https://discord.gg/5Hus6fM" /> + + + \ No newline at end of file From 358a20eb7786daa661ef60d20caca67d8eec105f Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:48:33 +0530 Subject: [PATCH 079/157] chore: refactor gradlelocalproperties and update gradle plugin (#957) --- app/build.gradle.kts | 2 +- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61a0634f..21f22dd1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,7 +69,7 @@ android { resValue("bool", "is_prerelease", "false") // Reads local.properties - val localProperties = gradleLocalProperties(rootDir) + val localProperties = gradleLocalProperties(rootDir, providers) buildConfigField( "long", diff --git a/build.gradle.kts b/build.gradle.kts index 34f141b4..ba87b6f4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.2.2") + classpath("com.android.tools.build:gradle:8.4.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc2d0f86..2968a1b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 7eec0eff02b1e7f8bd18a948515adae5fdc13f9e Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:41:06 +0200 Subject: [PATCH 080/157] Revert "chore: refactor gradlelocalproperties and update gradle plugin (#957)" (#1120) This reverts commit 358a20eb7786daa661ef60d20caca67d8eec105f. --- app/build.gradle.kts | 2 +- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 21f22dd1..61a0634f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,7 +69,7 @@ android { resValue("bool", "is_prerelease", "false") // Reads local.properties - val localProperties = gradleLocalProperties(rootDir, providers) + val localProperties = gradleLocalProperties(rootDir) buildConfigField( "long", diff --git a/build.gradle.kts b/build.gradle.kts index ba87b6f4..34f141b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.4.0") + classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2968a1b2..fc2d0f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From f775c1725d30d6f105dc114c8e6ecbfc6d2d56d9 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 8 Jun 2024 22:07:33 +0300 Subject: [PATCH 081/157] feat(TV UI): Subtitles Filter button focus fix (#1125) --- app/src/main/res/layout/dialog_online_subtitles.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml index d480bd34..e0eac5e0 100644 --- a/app/src/main/res/layout/dialog_online_subtitles.xml +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -107,6 +107,7 @@ android:layout_margin="10dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/change_providers_img_des" + android:focusable="true" android:nextFocusLeft="@id/year_btt" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" From 607a4510b6d941293fd29115818d79deb9b03681 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 8 Jun 2024 22:08:35 +0300 Subject: [PATCH 082/157] feat(Extensions): Trakt season names remove (#1124) --- .../cloudstream3/metaproviders/TraktProvider.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 07c9f316..8d149888 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -166,18 +166,10 @@ open class TraktProvider : MainAPI() { val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") val episodes = mutableListOf() val seasons = parseJson>(resSeasons) - val seasonsNames = mutableListOf() var nextAir: NextAiring? = null seasons.forEach { season -> - seasonsNames.add( - SeasonData( - season.number!!, - season.title - ) - ) - season.episodes?.map { episode -> val linkData = LinkData( @@ -250,7 +242,6 @@ open class TraktProvider : MainAPI() { this.comingSoon = isUpcoming(mediaDetails.released) //posterHeaders this.nextAiring = nextAir - this.seasonNames = seasonsNames this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) this.contentRating = mediaDetails.certification addTrailer(mediaDetails.trailer) From 3345326cb2987b98b87db0d93e291aa0d0b27b7e Mon Sep 17 00:00:00 2001 From: RowdyRushya <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:19:29 -0700 Subject: [PATCH 083/157] Extractor: VidSrcTo: better handling of runtime errors (#1121) --- .../cloudstream3/extractors/VidSrcTo.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index b9065688..2655670d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import java.net.URLDecoder @@ -26,12 +27,16 @@ class VidSrcTo : ExtractorApi() { val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return if (res.status != 200) return res.result?.amap { source -> - val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap - val finalUrl = DecryptUrl(embedRes.result.encUrl) - if(finalUrl.equals(embedRes.result.encUrl)) return@amap - when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) - "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + try { + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } catch (e: Exception) { + logError(e) } } } From 4c95610238d671cd8e11c3e85786a53e0db003c7 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sun, 9 Jun 2024 17:38:08 +0300 Subject: [PATCH 084/157] feat(UI): Hide Platform's not related settings (#1128) --- .../ui/settings/SettingsAccount.kt | 6 ++--- .../ui/settings/SettingsFragment.kt | 25 +++++++++++++++++++ .../ui/settings/SettingsGeneral.kt | 7 +++--- .../ui/settings/SettingsPlayer.kt | 19 +++++++++++++- app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/settings_player.xml | 6 +++-- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 27233525..3ec47648 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -298,10 +299,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - // hide preference on tvs and emulators - getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE) - - getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false if (deviceHasPasswordPinLock(ctx)) { 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 8ac17928..6ba93c0f 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 @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper @@ -53,6 +54,30 @@ class SettingsFragment : Fragment() { } } + /** + * Hide many Preferences on selected layouts. + **/ + fun PreferenceFragmentCompat?.hidePrefs(ids: List, layoutFlags: Int) { + if (this == null) return + + try { + ids.forEach { + getPref(it)?.isVisible = !isLayout(layoutFlags) + } + } catch (e: Exception) { + logError(e) + } + } + + /** + * Hide the Preference on selected layouts. + **/ + fun Preference?.hideOn(layoutFlags: Int): Preference? { + if (this == null) return null + this.isVisible = !isLayout(layoutFlags) + return this + } + /** * On TV you cannot properly scroll to the bottom of settings, this fixes that. * */ 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 ff891c43..22a7e098 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 @@ -27,10 +27,13 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -208,9 +211,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - // disable preference on tvs and emulators - getPref(R.string.battery_optimisation_key)?.isEnabled = isLayout(PHONE) - getPref(R.string.battery_optimisation_key)?.setOnPreferenceClickListener { + getPref(R.string.battery_optimisation_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false if (isAppRestricted(ctx)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 3d0bcb1f..20279cd1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -7,8 +7,14 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hidePrefs import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -31,6 +37,18 @@ class SettingsPlayer : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.settings_player, rootKey) val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + //Hide specific prefs on TV/EMULATOR + hidePrefs( + listOf( + R.string.pref_category_gestures_key, + R.string.rotate_video_key, + R.string.auto_rotate_video_key + ), + TV or EMULATOR + ) + + getPref(R.string.pref_category_android_tv_key)?.hideOn(PHONE) + getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.video_buffer_length_names) val prefValues = resources.getIntArray(R.array.video_buffer_length_values) @@ -227,6 +245,5 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } } - } } \ 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 deee5ad2..fad44ad4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -438,7 +438,9 @@ Actions Cache Android TV + pref_category_android_tv_key Gestures + pref_category_gestures_key Player features Subtitles Layout diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 82505511..5d5b11d0 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -101,7 +101,8 @@ + android:title="@string/pref_category_gestures" + app:key="@string/pref_category_gestures_key"> + android:title="@string/pref_category_android_tv" + android:key="@string/pref_category_android_tv_key" > Date: Sat, 15 Jun 2024 21:47:30 +0000 Subject: [PATCH 085/157] goodstream (#1133) --- .../extractors/GoodstreamExtractor.kt | 37 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 2 + 2 files changed, 39 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt new file mode 100644 index 00000000..9f6ba611 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt @@ -0,0 +1,37 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities + +class GoodstreamExtractor : ExtractorApi() { + override var name = "Goodstream" + override val mainUrl = "https://goodstream.uno" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + app.get(url).document.select("script").map { script -> + if (script.data().contains(Regex("file|player"))) { + val urlRegex = Regex("file: \"(https:\\/\\/[a-z0-9.\\/-_?=&]+)\",") + urlRegex.find(script.data())?.groupValues?.get(1).let { link -> + callback.invoke( + ExtractorLink( + name, + name, + link!!, + mainUrl, + Qualities.Unknown.value, + ) + ) + } + } + } + } +} \ No newline at end of file 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 c6cad804..5d696d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.extractors.Gdriveplayerorg import com.lagradost.cloudstream3.extractors.Gdriveplayerus import com.lagradost.cloudstream3.extractors.Gofile import com.lagradost.cloudstream3.extractors.GuardareStream +import com.lagradost.cloudstream3.extractors.GoodstreamExtractor import com.lagradost.cloudstream3.extractors.Guccihide import com.lagradost.cloudstream3.extractors.Hxfile import com.lagradost.cloudstream3.extractors.JWPlayer @@ -879,6 +880,7 @@ val extractorApis: MutableList = arrayListOf( Gdriveplayerorg(), Gdriveplayerus(), Gdriveplayerco(), + GoodstreamExtractor(), Gdriveplayer(), DatabaseGdrive(), DatabaseGdrive2(), From 30d223cfe3c65b8f104245d58056087e7913adbd Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 17 Jun 2024 04:01:14 +0300 Subject: [PATCH 086/157] feat(UI): Reorganize Settings (#1137) - Accounts Section & Remove "account" from title. - Security Section for Biometric that is hidden on TV. - Move "send logs" to "Action" section. --- .../ui/settings/SettingsAccount.kt | 6 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/settings_account.xml | 66 +++++++++++-------- app/src/main/res/xml/settings_updates.xml | 14 ++-- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 3ec47648..a8358d0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -299,6 +299,9 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) + //Hides the security category on TV as it's only Biometric for now + getPref(R.string.pref_category_security_key)?.hideOn(TV or EMULATOR) + getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false @@ -328,8 +331,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome for ((key, api) in syncApis) { getPref(key)?.apply { - title = - getString(R.string.login_format).format(api.name, getString(R.string.account)) + title = api.name setOnPreferenceClickListener { val info = api.loginInfo() if (info != null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fad44ad4..d9317ccd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -441,6 +441,9 @@ pref_category_android_tv_key Gestures pref_category_gestures_key + Security + pref_category_security_key + Accounts Player features Subtitles Layout diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index d1d18a0f..d165cd87 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -1,37 +1,49 @@ - + - + - + - + - + - + - + - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml index e3b36648..102f8ee4 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -1,13 +1,6 @@ - @@ -80,5 +73,12 @@ android:icon="@drawable/ic_baseline_construction_24" android:title="@string/redo_setup_process" app:key="@string/redo_setup_key" /> + From 7a0cd07dc19f3d1523292ef0569338b326cb1784 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 18 Jun 2024 06:02:32 +0300 Subject: [PATCH 087/157] feat(TV UI): Press Right to focus save on Logcat (#1136) --- app/src/main/res/layout/logcat.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/logcat.xml b/app/src/main/res/layout/logcat.xml index caa8c5cb..5cbb3f53 100644 --- a/app/src/main/res/layout/logcat.xml +++ b/app/src/main/res/layout/logcat.xml @@ -6,20 +6,20 @@ android:layout_height="match_parent"> + android:layout_marginBottom="60dp" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:nextFocusRight="@id/save_btt"> + android:id="@+id/text1" + android:padding="15dp" + android:textSize="15sp" + android:textColor="?attr/textColor" + android:layout_width="match_parent" + android:layout_rowWeight="1" + tools:text="Test" + android:layout_height="wrap_content"/> Date: Wed, 19 Jun 2024 00:24:35 +0300 Subject: [PATCH 088/157] feat(Extensions): Consider time zone in Trakt durations (#1140) --- .../metaproviders/TraktProvider.kt | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 8d149888..736e05f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -1,18 +1,39 @@ package com.lagradost.cloudstream3.metaproviders import android.net.Uri -import com.lagradost.cloudstream3.* import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.Actor +import com.lagradost.cloudstream3.ActorData +import com.lagradost.cloudstream3.Episode +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.NextAiring +import com.lagradost.cloudstream3.ProviderType +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.ShowStatus +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.addDate +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.mainPageOf import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.newHomePageResponse +import com.lagradost.cloudstream3.newMovieLoadResponse +import com.lagradost.cloudstream3.newMovieSearchResponse +import com.lagradost.cloudstream3.newTvSeriesLoadResponse +import com.lagradost.cloudstream3.newTvSeriesSearchResponse import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import java.util.Locale import java.text.SimpleDateFormat +import java.util.Locale import kotlin.math.roundToInt open class TraktProvider : MainAPI() { @@ -25,7 +46,8 @@ open class TraktProvider : MainAPI() { TvType.Anime, ) - private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktClientId = + base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") override val mainPage = mainPageOf( @@ -77,7 +99,8 @@ open class TraktProvider : MainAPI() { } override suspend fun search(query: String): List? { - val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + val apiResponse = + getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") val results = parseJson>(apiResponse).map { element -> element.toSearchResponse() @@ -85,6 +108,7 @@ open class TraktProvider : MainAPI() { return results } + override suspend fun load(url: String): LoadResponse { val data = parseJson(url) @@ -94,7 +118,8 @@ open class TraktProvider : MainAPI() { val posterUrl = mediaDetails?.images?.poster?.firstOrNull() val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() - val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + val resActor = + getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") val actors = parseJson(resActor).cast?.map { ActorData( @@ -106,12 +131,15 @@ open class TraktProvider : MainAPI() { ) } - val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + val resRelated = + getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } - val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true - val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isCartoon = + mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = + isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") val isBollywood = mediaDetails?.country == "in" @@ -163,10 +191,11 @@ open class TraktProvider : MainAPI() { } } else { - val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val resSeasons = + getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") val episodes = mutableListOf() val seasons = parseJson>(resSeasons) - var nextAir: NextAiring? = null + var nextAir: NextAiring? = null seasons.forEach { season -> @@ -208,7 +237,7 @@ open class TraktProvider : MainAPI() { rating = episode.rating?.times(10)?.roundToInt(), description = episode.overview, ).apply { - this.addDate(episode.firstAired) + this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { nextAir = NextAiring( episode = this.episode!!, @@ -251,7 +280,7 @@ open class TraktProvider : MainAPI() { } } - private suspend fun getApi(url: String) : String { + private suspend fun getApi(url: String): String { return app.get( url = url, headers = mapOf( @@ -286,14 +315,14 @@ open class TraktProvider : MainAPI() { return "https://$url" } - private fun getWidthImageUrl(path: String?, width: String) : String? { + private fun getWidthImageUrl(path: String?, width: String): String? { if (path == null) return null if (!path.contains("image.tmdb.org")) return fixPath(path) val fileName = Uri.parse(path).lastPathSegment ?: return null return "https://image.tmdb.org/t/p/${width}/${fileName}" } - private fun getOriginalWidthImageUrl(path: String?) : String? { + private fun getOriginalWidthImageUrl(path: String?): String? { if (path == null) return null if (!path.contains("image.tmdb.org")) return fixPath(path) return getWidthImageUrl(path, "original") From b702b7b1ecfc254dd9b3f8a408a8092452c0cf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Misael=20Jim=C3=A9nez?= Date: Wed, 19 Jun 2024 07:40:23 -0600 Subject: [PATCH 089/157] Fix DoodExtractor. (#1134) Fix StreamWishExtractor --- .../cloudstream3/extractors/DoodExtractor.kt | 19 +++++- .../extractors/StreamWishExtractor.kt | 60 +++++++++++++------ .../cloudstream3/utils/ExtractorApi.kt | 15 +++++ 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 8dcfb859..370dcaca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,6 +7,18 @@ import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.getQualityFromName import kotlinx.coroutines.delay +class D0000d : DoodLaExtractor() { + override var mainUrl = "https://d0000d.com" +} + +class D000dCom : DoodLaExtractor() { + override var mainUrl = "https://d000d.com" +} + +class DoodstreamCom : DoodLaExtractor() { + override var mainUrl = "https://doodstream.com" +} + class Dooood : DoodLaExtractor() { override var mainUrl = "https://dooood.com" } @@ -56,9 +68,10 @@ open class DoodLaExtractor : ExtractorApi() { } override suspend fun getUrl(url: String, referer: String?): List? { - val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/... - val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... - val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) + val newUrl= url.replace(mainUrl, "https://d0000d.com") + val response0 = app.get(newUrl).text // html of DoodStream page to look for /pass_md5/... + val md5 ="https://d0000d.com"+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... + val trueUrl = app.get(md5, referer = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0) return listOf( ExtractorLink( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt index 77d98e49..551d1ef6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt @@ -1,34 +1,56 @@ package com.lagradost.cloudstream3.extractors +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName + +class WishembedPro : StreamWishExtractor() { + override val mainUrl = "https://wishembed.pro" +} +class CdnwishCom : StreamWishExtractor() { + override val mainUrl = "https://cdnwish.com" +} +class FlaswishCom : StreamWishExtractor() { + override val mainUrl = "https://flaswish.com" +} +class SfastwishCom : StreamWishExtractor() { + override val mainUrl = "https://sfastwish.com" +} open class StreamWishExtractor : ExtractorApi() { override var name = "StreamWish" - override var mainUrl = "https://streamwish.to" + override val mainUrl = "https://streamwish.to" override val requiresReferer = false - override suspend fun getUrl(url: String, referer: String?): List? { - val response = app.get( - url, referer = referer ?: "$mainUrl/", interceptor = WebViewResolver( - Regex("""master\.m3u8""") - ) - ) - val sources = mutableListOf() - if (response.url.contains("m3u8")) - sources.add( + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val doc = app.get( + url, + referer = referer, + allowRedirects = false + ).document + var script = doc.select("script").find { + it.html().contains("jwplayer(\"vplayer\").setup(") + } + var scriptContent = script?.html() + val extractedurl = Regex("""sources: \[\{file:"(.*?)"""").find(scriptContent ?: "")?.groupValues?.get(1) + if (!extractedurl.isNullOrBlank()) { + callback( ExtractorLink( - source = name, - name = name, - url = response.url, - referer = referer ?: "$mainUrl/", - quality = Qualities.Unknown.value, - isM3u8 = true + this.name, + this.name, + extractedurl, + referer ?: "$mainUrl/", + getQualityFromName(""), + extractedurl.contains("m3u8") ) ) - return sources + } } } \ No newline at end of file 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 5d696d33..1302453a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.extractors.BullStream import com.lagradost.cloudstream3.extractors.ByteShare import com.lagradost.cloudstream3.extractors.Cda import com.lagradost.cloudstream3.extractors.Cdnplayer +import com.lagradost.cloudstream3.extractors.CdnwishCom import com.lagradost.cloudstream3.extractors.Chillx import com.lagradost.cloudstream3.extractors.CineGrabber import com.lagradost.cloudstream3.extractors.Cinestart @@ -106,6 +107,9 @@ import com.lagradost.cloudstream3.extractors.Odnoklassniki import com.lagradost.cloudstream3.extractors.TauVideo import com.lagradost.cloudstream3.extractors.SibNet import com.lagradost.cloudstream3.extractors.ContentX +import com.lagradost.cloudstream3.extractors.D0000d +import com.lagradost.cloudstream3.extractors.D000dCom +import com.lagradost.cloudstream3.extractors.DoodstreamCom import com.lagradost.cloudstream3.extractors.EmturbovidExtractor import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX @@ -227,7 +231,10 @@ import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.extractors.EPlayExtractor +import com.lagradost.cloudstream3.extractors.FlaswishCom +import com.lagradost.cloudstream3.extractors.SfastwishCom import com.lagradost.cloudstream3.extractors.Vtbe +import com.lagradost.cloudstream3.extractors.WishembedPro import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -777,6 +784,9 @@ val extractorApis: MutableList = arrayListOf( DoodSoExtractor(), DoodLaExtractor(), Dooood(), + D0000d(), + D000dCom(), + DoodstreamCom(), DoodWsExtractor(), DoodShExtractor(), DoodWatchExtractor(), @@ -854,6 +864,7 @@ val extractorApis: MutableList = arrayListOf( Guccihide(), FileMoon(), FileMoonSx(), + Vido(), Linkbox(), Acefile(), @@ -909,6 +920,10 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), + WishembedPro(), + CdnwishCom(), + FlaswishCom(), + SfastwishCom(), EmturbovidExtractor(), Vtbe(), EPlayExtractor(), From afa178a63a7173316cc04fbbd3fb989f77a06515 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 19 Jun 2024 17:06:08 +0300 Subject: [PATCH 090/157] feat(TV UI): Accounts PIN login support (#1123) --- app/build.gradle.kts | 1 + .../cloudstream3/syncproviders/OAuth2API.kt | 16 ++ .../syncproviders/providers/AniListApi.kt | 1 + .../syncproviders/providers/DropboxApi.kt | 1 + .../syncproviders/providers/LocalList.kt | 1 + .../syncproviders/providers/MALApi.kt | 1 + .../syncproviders/providers/SimklApi.kt | 55 +++++++ .../cloudstream3/ui/result/UiText.kt | 13 ++ .../ui/settings/SettingsAccount.kt | 141 +++++++++++++++--- app/src/main/res/drawable/cloud_2_solid.xml | 8 + app/src/main/res/drawable/example_qr.png | Bin 0 -> 46354 bytes app/src/main/res/layout/device_auth.xml | 59 ++++++++ app/src/main/res/values/strings.xml | 6 + 13 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/cloud_2_solid.xml create mode 100644 app/src/main/res/drawable/example_qr.png create mode 100644 app/src/main/res/layout/device_auth.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61a0634f..fc2e9131 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -217,6 +217,7 @@ dependencies { implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview + implementation("io.github.g0dkar:qrcode-kotlin:4.1.1") // QR code for PIN Auth on TV // Extensions & Other Libs implementation("org.mozilla:rhino:1.7.15") // run JavaScript diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt index ef74edfc..3d0bb940 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt @@ -5,7 +5,23 @@ import androidx.fragment.app.FragmentActivity interface OAuth2API : AuthAPI { val key: String val redirectUrl: String + val supportDeviceAuth: Boolean suspend fun handleRedirect(url: String) : Boolean fun authenticate(activity: FragmentActivity?) + suspend fun getDevicePin() : PinAuthData? { + return null + } + + suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean { + return false + } + + data class PinAuthData( + val deviceCode: String, + val userCode: String, + val verificationUrl: String, + val expiresIn: Int, + val interval: Int, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 5c02e7f7..0551fe6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -32,6 +32,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override val redirectUrl = "anilistlogin" override val idPrefix = "anilist" override var requireLibraryRefresh = true + override val supportDeviceAuth = false override var mainUrl = "https://anilist.co" override val icon = R.drawable.ic_anilist_icon override val requiresLogin = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt index 7ec168da..94537ea3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt @@ -11,6 +11,7 @@ class Dropbox : OAuth2API { override val key = "zlqsamadlwydvb2" override val redirectUrl = "dropboxlogin" override val requiresLogin = true + override val supportDeviceAuth = false override val createAccountUrl: String? = null override val icon: Int 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 7552fe9d..00f8d00c 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 @@ -21,6 +21,7 @@ class LocalList : SyncAPI { override val name = "Local" override val icon: Int = R.drawable.ic_baseline_storage_24 override val requiresLogin = false + override val supportDeviceAuth = false override val createAccountUrl: Nothing? = null override val idPrefix = "local" override var requireLibraryRefresh = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index fdbe763a..4249f949 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -40,6 +40,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private val apiUrl = "https://api.myanimelist.net" override val icon = R.drawable.mal_logo override val requiresLogin = false + override val supportDeviceAuth = false override val syncIdName = SyncIdName.MyAnimeList override var requireLibraryRefresh = true override val createAccountUrl = "$mainUrl/register.php" 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 08c8588b..4385fa5e 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 @@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType @@ -45,6 +46,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override var name = "Simkl" override val key = "simkl-key" override val redirectUrl = "simkl" + override val supportDeviceAuth = true override val idPrefix = "simkl" override var requireLibraryRefresh = true override var mainUrl = "https://api.simkl.com" @@ -267,6 +269,21 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } + data class PinAuthResponse( + @JsonProperty("result") val result: String, + @JsonProperty("device_code") val deviceCode: String, + @JsonProperty("user_code") val userCode: String, + @JsonProperty("verification_url") val verificationUrl: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("interval") val interval: Int, + ) + + data class PinExchangeResponse( + @JsonProperty("result") val result: String, + @JsonProperty("message") val message: String? = null, + @JsonProperty("access_token") val accessToken: String? = null, + ) + // ------------------- data class ActivitiesResponse( val all: String?, @@ -1045,6 +1062,44 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { return simklUrlRegex.find(url)?.groupValues?.get(1) ?: "" } + override suspend fun getDevicePin(): OAuth2API.PinAuthData? { + val pinAuthResp = app.get( + "$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}" + ).parsedSafe() ?: return null + + return OAuth2API.PinAuthData( + deviceCode = pinAuthResp.deviceCode, + userCode = pinAuthResp.userCode, + verificationUrl = pinAuthResp.verificationUrl, + expiresIn = pinAuthResp.expiresIn, + interval = pinAuthResp.interval + ) + } + + override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { + val pinAuthResp = app.get( + "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId" + ).parsedSafe() ?: return false + + if (pinAuthResp.accessToken != null) { + switchToNewAccount() + setKey(accountId, SIMKL_TOKEN_KEY, pinAuthResp.accessToken) + + val user = getUser() + if (user == null) { + removeKey(accountId, SIMKL_TOKEN_KEY) + switchToOldAccount() + return false + } + + setKey(accountId, SIMKL_USER_KEY, user) + registerAccount() + requireLibraryRefresh = true + return true + } + return false + } + override suspend fun handleRedirect(url: String): Boolean { val uri = url.toUri() val state = uri.getQueryParameter("state") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 0e8160db..e0762cc5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.result import android.content.Context +import android.graphics.Bitmap import android.util.Log import android.widget.ImageView import android.widget.TextView @@ -84,12 +85,14 @@ sealed class UiImage { ) : UiImage() data class Drawable(@DrawableRes val resId: Int) : UiImage() + data class Bitmap(val bitmap: android.graphics.Bitmap) : UiImage() } fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) { when (value) { is UiImage.Image -> setImageImage(value, fadeIn) is UiImage.Drawable -> setImageDrawable(value) + is UiImage.Bitmap -> setImageBitmap(value) null -> { this?.isVisible = false } @@ -107,6 +110,12 @@ fun ImageView?.setImageDrawable(value: UiImage.Drawable) { this.setImage(UiImage.Drawable(value.resId)) } +fun ImageView?.setImageBitmap(value: UiImage.Bitmap) { + if (this == null) return + this.isVisible = true + this.setImageBitmap(value.bitmap) +} + @JvmName("imgNull") fun img( url: String?, @@ -129,6 +138,10 @@ fun img(@DrawableRes drawable: Int): UiImage { return UiImage.Drawable(drawable) } +fun img(bitmap: Bitmap): UiImage { + return UiImage.Bitmap(bitmap) +} + fun txt(value: String): UiText { return UiText.DynamicString(value) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index a8358d0d..d227f9f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -1,12 +1,16 @@ package com.lagradost.cloudstream3.ui.settings +import android.graphics.Bitmap import android.os.Bundle +import android.os.CountDownTimer import android.view.View -import android.view.View.* +import android.view.View.FOCUS_DOWN import android.view.inputmethod.EditorInfo import android.widget.TextView import androidx.annotation.UiThread import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity @@ -21,6 +25,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.AccountManagmentBinding import com.lagradost.cloudstream3.databinding.AccountSwitchBinding import com.lagradost.cloudstream3.databinding.AddAccountInputBinding +import com.lagradost.cloudstream3.databinding.DeviceAuthBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi @@ -31,6 +36,10 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlAp import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.ui.result.img +import com.lagradost.cloudstream3.ui.result.setImage +import com.lagradost.cloudstream3.ui.result.setText +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -51,9 +60,13 @@ import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import qrcode.QRCode +import java.io.ByteArrayOutputStream class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { companion object { @@ -134,7 +147,109 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome try { when (api) { is OAuth2API -> { - api.authenticate(activity) + if (isLayout(PHONE) || !api.supportDeviceAuth) { + api.authenticate(activity) + } else if (api.supportDeviceAuth && activity != null) { + + val binding: DeviceAuthBinding = + DeviceAuthBinding.inflate(activity.layoutInflater, null, false) + + val builder = + AlertDialog.Builder(activity) + .setView(binding.root) + + builder.apply { + setNegativeButton(R.string.cancel) { _, _ -> } + setPositiveButton(R.string.auth_locally) { _, _ -> + api.authenticate(activity) + } + } + + val dialog = builder.create() + + ioSafe { + try { + val pinCodeData = api.getDevicePin() + if (pinCodeData == null) { + showToast(R.string.device_pin_error_message) + api.authenticate(activity) + return@ioSafe + } + + /*val logoBytes = ContextCompat.getDrawable( + activity, + R.drawable.cloud_2_solid + )?.toBitmapOrNull()?.let { bitmap -> + val csLogo = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, csLogo) + csLogo.toByteArray() + }*/ + + val qrCodeImage = QRCode.ofRoundedSquares() + .withColor(activity.colorFromAttribute(R.attr.textColor)) + .withBackgroundColor(activity.colorFromAttribute(R.attr.primaryBlackBackground)) + //.withLogo(logoBytes, 200.toPx, 200.toPx) //For later if logo needed anytime + .build(pinCodeData.verificationUrl) + .render().nativeImage() as Bitmap + + activity.runOnUiThread { + dialog.show() + binding.apply { + devicePinCode.setText(txt(pinCodeData.userCode)) + deviceAuthMessage.setText( + txt( + R.string.device_pin_url_message, + pinCodeData.verificationUrl + ) + ) + deviceAuthQrcode.setImage( + img(qrCodeImage) + ) + } + + val expirationMillis = + pinCodeData.expiresIn.times(1000).toLong() + + object : CountDownTimer(expirationMillis, 1000) { + + override fun onTick(millisUntilFinished: Long) { + val secondsUntilFinished = + millisUntilFinished.div(1000).toInt() + + binding.deviceAuthValidationCounter.setText( + txt( + R.string.device_pin_counter_text, + secondsUntilFinished.div(60), + secondsUntilFinished.rem(60) + ) + ) + + ioSafe { + if (secondsUntilFinished.rem(pinCodeData.interval) == 0 && api.handleDeviceAuth(pinCodeData)) { + showToast( + txt( + R.string.authenticated_user, + api.name + ) + ) + dialog.dismissSafe(activity) + cancel() + } + } + } + + override fun onFinish() { + showToast(R.string.device_pin_expired_message) + dialog.dismissSafe(activity) + } + + }.start() + } + } catch (e: Exception) { + logError(e) + } + } + } } is InAppAuthAPI -> { @@ -227,23 +342,15 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null, ) ioSafe { - val isSuccessful = try { - api.login(loginData) + try { + showToast( + txt( + if (api.login(loginData)) R.string.authenticated_user else R.string.authenticated_user_fail, + api.name + ) + ) } catch (e: Exception) { logError(e) - false - } - activity.runOnUiThread { - try { - showToast( - activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail) - .format( - api.name - ) - ) - } catch (e: Exception) { - logError(e) // format might fail - } } } dialog.dismissSafe(activity) diff --git a/app/src/main/res/drawable/cloud_2_solid.xml b/app/src/main/res/drawable/cloud_2_solid.xml new file mode 100644 index 00000000..3810b4bf --- /dev/null +++ b/app/src/main/res/drawable/cloud_2_solid.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/example_qr.png b/app/src/main/res/drawable/example_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..18decbac49533dcd2890ac8112305b19d05cfa11 GIT binary patch literal 46354 zcmeFa3pmtk_cxv~gJ$H^FvwvhVn_xlr-X5?90nDoCgqfBqm !=O#FGnJfjN;>Qk zwbM?{6~dm*X&0jqB_x$8nRnfz_S617`}zHz|MmR;*L%J1e_z*r_S3by=lfmvTI*hG z-D`c;`W|q1b)17&#?P8HYmT#%oyV+Mq5||k2{HIDM=`N$;QvH+c{tK%o$pi`nl+0& z%h}GxYwyP4XRmiEudie?UVl-4qD`-GBW|E?e9W5qQTa>mqQgKmZV{6RXm#bkj4%CnK)EzRLly=+DLiP1^x^Y7~ z?X&uR6@BxZDRJeVS5`-|jQML=AN;zd66g*(f>7;+>Jk{M6l+T9{*ilAU)(%)PODuz zF-;49doOkR%WqBW;`bk4MRmq)4Z72}i_?FK&I^*E`g6twi+w-o$n0H-#Zy+#TGOgi z!8HrvV@T^AiQ`2txLwYBt6p^YEZ7?dEJ^JA2VbAsyWf2}XbJBbCDQ>B5RXqG z>JvLzq>z(Wcxm+I_b(YPakG*3JaK)X10xj}Z#_rInkB2V#W580@c5Fjr!VMy+TNly z2KZWPpRLP0&v(QLgZEwAy7hRQ#3b$uS;F*WN0DXd=oO}Dj~oHK0$1|)EB08e_53TQEEkP_JjB>=M(FkQJ{2=?EW}+Bt^cgW?;L-*p2s;wo&U9 zlo#7Syo*{Z!%LIrbiT`BsdXm);)aKSjW;SIk@uhu|G7B6smE}zE_y9?lFqNY^Gjy* z+Oz5(cg;TiV&}*7RzmdNn=i)~Q{eeEb203g$_g6Zv?l9(?9n*Kcp4|JU(mC!PkWiH z2<5$9nDk@`A=;F@-60tQxG@hb{^S+ma?ak+qcf|mLhtR8rj~Yg2n72-vD6GO!qBlt z!<~Aay^m3!u!d4$FMKRht5jCTV{7!r<-Ru3Gy%6;24cgW!2@sf2xi`E}foJ7y0=DP;9blpBjoztqxijO(=Y@7fCrW@bEW_rI^^FsfB`D zAI5%nqm_0SJWpA^$wpdw&hQNse7dH7TRgYzH^*r)ky`iZBXP1nIaoWL9E#$H;Vf&W zY-4QEbH!~7-J=PJa}A|io@#50q<{*KnNG8yS77eFiq{Oo_!cU~nVp0Wdf-IMTTY`BQ`m3I? zm@9%`3sWMS?!Qbx0=XwaToLNWB!*3H{ozZ^mKn0+woBK2nV`RVqqBcdRAFL_Tfb2f zsYlYc#im75v3;R3JVHN{PmJ51=o%a8Fw=B@ zrV-WBcauJEOs-Kdre$=dzVUtb=gC3IUE|Jpetm8%ql&_e7)d$QBeFD!lPTvBgPmm2 z;GYoX$DJA_=EKD5!9qg|XWqH#)m%}Pz}?<4kqqBts!tJW3THob-C*~(Q%t;d!8`SlA0ZE=gl@4lm&Wn^EnPDy@|mO)F_ zESw_63pOXyyVB_I#xM5yYiskL_m9_oPL7owXV-mDpWh^Sz|=aKL}dCmD=H@;CbSfX zd6SF}q+u}mO8A^U7q@APjKN`-+UieqzINL>YvRDg8h&wM(-XeXBR^Nr@=WfQ#AI|Sf&#HA=9djWh2MX; zdGj8%=&(6tD77{?upv;toii95ER!{m;3l2n;Q zV(})TxBE_Wy@>Gy6Fv)hdM5uuzjd_Qf?Kj#3WMA*c;1an><`^*8mJY8eG%S+eWIb` zIOKxr#+kPhCYG(p(9l}ME$W>FDk-x;$9Q$wMeZMpxiztRW?ik$wR^8ljn-0_NwK{b zzNEB_#l1h~tV6j$QZh^y>jggJE2%0EaW2n`R`oEQKUQE;}FhMEbvS5_O~^{8g53g+lAH=yX@ z!qlUkWXnT`U!*z{B7#kU%FRxgwm6YVS|F8kooY-|U+KDM=V97LeZjTR77~RyxlKSZ z0yzhGtZ;g3?~q<(gZRwS!>8=F$*QK3xyOqNox3Oc>RzOFcK4;71ql|P!`b<%-Oi=q z&ChkzBPn0%&D8x~c*C-Aj06ty8Bf-&YVWj_&dBz`E$zui3QTSO^C@lfcF3oHe%nsei{g`G2ZzsLNU@BtfR_b& zgOpx^8>sQzIr+|V1CmpVGci)Q)kFmck(5SBU~Wc6VTgk<-$)t zl#R!|w`WQATV;7naA&_}QSpS5cw0Y2q$v?B6HCs#ZnlSYqLxK8HH^{H%A(;H!*0&6 znLZ+*YxM=nc;sj5rZ$N%HYI3TcD)Aj4x}HJ9&gAJtkd7|GJI{M(FzDuZ~GW7&CPt3 zbU#~SY3Oi;euZ^b#^(&FZCNc;K3SauYjp%M4su@_>-eL3lDDtTT|ax5*tC+9ND51L zZ&f{A`z#@tXsn}-Us2ue&b1YplQTS6RF7j4eY?7z;Vd=5CCm-bRyNPlY(G5}gk^Xu zDj6h;>+Id7$XeCaWN$M&zjE!@J@`eEd`}1U?-xZD*3~#d>*6KQL1daZSI@`eQaY75o{hUOGmK ztrUDWoXPduA*S@;YCrx7=tSlN_}ge|yR~$F`gmVY);}@&edAKX`D}VS`iSCuEaE z=iA=60Ga^27ivPlg?KTSJrnM{;>;{yKIol$kJq%VKelZng^XV;aT?5XbeJ{KyRdZ; z0q3Hyv$CYU%NZY?ZEwB2CFMGXU(9REFd;-!hh6GE{EvF)7EK7|`a4SF^zmjsWA=Vx z(SuJPAUD#I^i-05X08V=T&Uuu+A*rC54C<`jN2^V3r^K|7lLrCM0I%X?zm6| z??M0FA_rMo{kArEe-b#|Y3akYZ6p=^aWs@BjmqO~ZRFW( zMtzZisEhds0Zcx*#Qch{%{(v$mA}H@O0$&)i?SF6Q!)R<=>OT4T$qYHOfhzD%qy4* zB7T1gf4f2pmLuU7_zG>_j1qwyfY?Y{DiuR8z8X+e*h+WZ!!fgP zwvn!qXc6aEtR*8m@iq{)c75p4^o3fd%%Z6vj~~^{{Wi$h`}q;WsO|3Xwv?H-y>t%! zYG3Rr+Jsu+F}$`8j2Pi~X0DCXZu%Lrl(=02E@#63LP^sTO`{ddTg2U_w_UxIe`~|D zRw_h9w4fW0w<8VRGXHVS#in2DF~DmJ^K&lqOZpOH-C=F%)Tom41VJ!RS7o3#>biYF zyV7J385sp$2ZoS;PQQ@BEsg!>a~cp7Nt(!n1jqO1)XvY(zg|(Tl-J=WVH6+@Y>sH# z_;nBLXB)y!799CJM*_SSGC$3CUqPsY7w+I`r90Q3qHWQZmtR_#;)4a^9xu!Uu?+96 zDoknYo(L6kOLp(w-wzz<-oV9m9Y4|?&{GZ9-aNb`b{+r_~_q|3npYS_{(}w4h$q*W{hi5?dhqM775OEx-}8} z7z92O?%73flwujYVgqC!Rq#2@;5_!Md2`#Qdw8^D+!sC?=Rsp9~dHk_{JbL<0=CfxDgmg^x3ef2(DRR z!|{Bu$KMX_uC?fHLYcc0jIpazf3u;ndvB`FF zBINuxXWn*TOECG?Q<74pbU&!4HdN@Cigw+W=OWq&D}?-=ae%HyNm{aC$MLP3BZ#-2Qm3w((15rf z|3l(_0e9b;3xd}wf(Qz!)%Tps-139OwyIXSM?#i5ORG)gSCrMdQn&jmudKoV6?$mWlHkJ*5 zj=!o3?YSZp5Emi=aJsFzaFm77UDWt!Yn-+cJqwAI?QdxQHZ|vmPcQ(m%d>E3{9}H6G*<+8fOV#16T)&IvD0Kh+7OqEh9!k zkp1(g{{U8b_USy+WHDV8LmX4G;Y7^u5Ed2AL=wY#Zqpyl`&{4(LW}K{p5CpH4EYNB znUaJjnxJOHpOtA}Yj;;h*9$di5JFyKL?;BK5P)izEaQLd69zd*|LB#9sy7Tx@Npv@ zNcrafXT+UD!OM8yEfO0{UKZo~kOPUhFMA$l`6ndb!k-@C6wHxju`bC(af&H2A#iph zFT+KIWJEDZgrL{URmE0RJ|mk&OXmG{eFvOb_pf&4sJ7rACPG|bPOx>FtwVWm$9l(M zP_rM`96WY93Zkb^!@qxAB-EscAFg7Q5p59B{^6#3oRhhl$lFkS(lefC&tqGC96-wV z9}@Qv-5jE-UZgc@PH<=3+V^7J**-@^eIJr37p@@$$X`Hw#AtzV@|%6l|C86$)y^nN zz&oyLAlZ(+?EVD|3KiYr3$Yfrvjo?rqO#y>MPN=Q#^tjqrC=&w1s5)L2Gz1Zd-L}F zWjM=K`#!w+tS}RYUhzetK~z;?QYVbC~cpeum}UEM%*`|n!3sZ`~ukKJy*t=+#-}ZhzKsQ#6SS>*auXY zrd|wbcPYc_!7xTPczbm7z^8bLi09~4gn@yFI||-3!prY41``|prSdVDsifTvih+Y< za%mVNCg;AN|DMAvlOS2}|EzG!f#u81OY>q619y~G*Z1~$OuQ9{3)4}Wlv01YIU-S4~oRUU&sQ$K}7k~s1FZu(DU&# z9173Z(uggaR!VG#@{g+J_w1SBBXUM%w(o{zsly|Uoe)XH@n!6-opz@-*r$L3!*M`C z9^BDQx&8_4+mdiOLE%6NkOxJu*T-&5^O)M_2saIE#YHIWv?upUr8KIth&5(u)-RvQ zr@uCQDLoxv=L6AF3EtG-&My;->KQ!}45tOLTT|zN5H$W-2!3zUp3oiM2D%&{=tXss zW1^@|lA`v#wD8G7Y1IWKASG~v+i7fX&lK~zJ2`O?RJ~ve6Y*(MV2R*(@!Yc;M%GLd z%)KB|%8}}*axAR4MzB~3v1z|85g17k`;L*ekj3FyXWpUZRAjKDo(?W za%AizWhv-UhDz9-Z!sW#@#AN0aW*NJX(0rq8@WpYa#DneK`B&@QH}n7AypAAY!ejV zy3gs{TL!v0#zuD=dV=IHN%ab_g*&49=DtKN9mtG;hrPC*PaAZH{cSTh{!J~RaYTPu z4>3}jff%XyJtNt+XW~U&6ueP_z#qkK=4T9?MMP?=_xlWkgK7MAljPzE85xi2a!Z$2 z-=ElOwOt^52M(x&(V2K#2cnoWS(u8JVS6k4gQe;07%3%&qGkdrwVBX~ z9*>8FHo>&srt5c_G*lNH<)MOVi8+ur zu}$C)1@x!3WLb{{W)M5EWKEanhrT|MjCG%%XO%V{8_-`UVNL$kAXb|F ze5DMNHF-C-|Hm<9c-|G1fk4kQESS1bZ9TL)dY5B5O?;o(;O&MDv7}nf4P81LMH8wh znbiQ=s%q_g$*woY9LU`H)M7;SW9y5J%O{Omj@j%N-Tb9^Yy~{{#ZV0*stW`Bw%oYhvh#lk7 z<3c49mH~yxhQm>SbprOm({P(47`>H~NQaG_Jt8oizHW#e2i~CM;@5SgrDfpsXf4ev z$p*>R&mlv3Yx3hwnHG&q37we-q31ClXM;1WBhRHJ)GJ~RK!~FLqGz5h`H=PeQeidY zD`8u7|0ZmPvww5!|K~dPiq2QxQa=x1C{bAaEnl@#(wuDXbqTb@jo6P@LKD+4guwWE zwFH*QTZG%;sbSRHtik~g6M*Fiq%7DaN7eMB>!FmYl~At$s|?X9YyK&;HdBHcqxuBz zxX6+!P<714VhX12A?%1A*dHqeW*NJFNXGqxIcK4u`tkhwTvf~L(nM=WSOvpUnQ}_u z)FVYhX;_~a&$=UP*IRxP5^)w82D9Pk!smNSPzAT1-@YVcs)}jbn6k599nztZ#+wdn zVc2=w=Wo%Kur7>;;?SlBNr0ei0D?125jA_qd%}~P$-GvpIk1sibnE|04J33*M6v)i z+vh)5$X&^`w7*Hw;+nOL| ztb+o)kl+sz#GP_9gg6?3ZQT>Yk|Cbxdd>AT4H>e5(o>_RjQjLfgalH+vl1D)Qyux}hM_Jv`W;sR}RZvT4$k0dTBHf8EVgz!@dX!xGao$p7i4M&=hzMK&{l z7^v)IXsp7Bxcw_?*pShilU;fMhHW0-EKLsMj?xrF8l=|RL=QcOb z)o^cVo!c~80?HK!N}(+aF6bcI)~T@jcN1r%P~ljZnY9;TmBOThS_$#vZlWN-i@>8pH;!$kgqdSG*QNT~tn`187z=ci)*2$ii=>pb;3**>f+Y^^ zqTsB_g_|O<|LLZ}NWg223`7j*mWrR8#NK~N%`*OIaJ{Arl9Sy{FOZW6{I;H@1b8sF zY3ag*M#k&gWcbr2d5r?a1iEX1;(m2ijLf7crp@XAXyR71ElYHyKvO{q32?v5YbGE_ zR19qElE`0Z7AH;_v?_xgKOU!7yNZx-k5V|io4R&7{GxrliAA)5vpBqH4;Ky%+1=Kf zf5C7wpyYUBBDfNb)cwYCCeyj9Dnj5Q03 zhu`89+5yqFcRX&^9C!#*EDHW~R`G2!9G3CsvCo4bb9EUfPTVbD$5ZT+_R?S=?CzY| zYVB~x-zAG31u$?PZ$A|vCgoDfaGi^#lmsrR*caJf*JQu>Fw2ZUIYMrdQNm6_or~rh zA+^n?gQSYp&*J*0{6t?9$<59$FE>LK zRw=m!E;>rOv9d3K_vuB>c%{|xO<&wM6}`g+h#!Q)U2P_#O8If^wegcZwvRHp$m4Obif%upqJ#ulC<@Q8@8l8L|r{l zTR%*9kmrS20g7bWLJtn2KK>mNGkq~pO5(PKrbJ2Xe83=3c*VQCZ->+llDRr&OIAU4 z)c^hcpbOVSDQAb^(*76nW79Un;Yu|$c7_{zm{1zEg~JH$)Ic%{StI&;B+%=tsil)$ zCF3#MNb)DZ3>tX>Eagu-2TKdd5D>c30|p59-`X{Z@TR?=8ft~xX6ErWrSDJRAjZ&Di4DoN~xT`;{3!}O#kvR`*g6L zhRIqw{$3(dpdai`5=YfIP+Mn`?$8mk>GKG%Ijva}w@ZPpYm-bdk{Gj3#zR>9r=5e4 z74(JLZhi*X{BP|VTxm76y#x%kM2EX6S#LCxoIZYF7BeC#KCRPl(OG_RqV=i1`vy?7 z$b0}z7$0ic=gJ_X@K>U)O?}%}r*V_33F(9SUUdHWtjd3!jO{%e_@iMzWtVR1q&qEW zF|Y$rZHqg?;C{dJKb?$!ch?+&GKn39JaLf{UOcH^RW6CKT$Y!XArp4T>|DSSW`xWj zCr?=mlFOCRuc{t(U&(wh0ja&e5OpI7u2hlDiaDj344mmxX~+#S-S^Cx1f?Zs6W&|{ zdjIJxq3rqhI|r}nUA@XG(j&Zkg6hcG}1}Nfo~U2GpEZEbtfXVRDeklggu#$@QNcIX+rtqJ z|BWLpO&O<32paIdGs)&6Dm*GUC6_Bsvbe5b2cZGYSG{? zm-ene3<}dWCl1_zI=$7&_%xOVD)s;_YB&wj(<8ubT)&{~wT=L?myd0o0NX{9+# z7Lns)N+p;bNb59Q3$F9*21OaYh!6G+r2Rl#D@F+yBLNu7K%^Wo`c(hj zP~z7?yoYj%4Zr8i#X=Au{xb%^ciIAGsmK-s2S4ZRBA;ji9|_}AGf;qll0kBSS?%wc zbI)1R_vwqqtT?yn&`S)eB3rarWn)n8K`jeryZoiKEeKNp7@#@#+cq}j8l8E!z^Oq4 z(so5vG&w%6XNqt-mx-G_MLEkdG-m|JrBzj3!iVVfK~4{WOvIo;4w$^*&H3~HQi3$+ z@H?$gbYUy)t=i?LP+JH%B_6>S;2^*<+SC)l>MWcQ{sVNoDYUyUybi@RDDAI;zZar7 zwR&A@%fDzjHK?Lgtd>5V&ufiKk^Cxjz0OXPhX6E#($)97Xj}AB#9>js`RDmRNw?KO z-L50k=t#wRD95xmKZiWn36FaF23b^}+U531uJi@LmpA|8g43W)uUVE!0I^(rf5nB8 zUrL;PdBsPiRUHDgdpkNUhmPjHr!OkOck3B6k{+dmK!e~pB~iq>K$(a6J%7?Z@<+Rc zW9!X8|4vOkx_cTivv7(4l`}~l&)KhYOb9uv^g68@>g}~rX)=%pkN25mRf243VGP84 zeC-$jL2~pm85Jpjb=;J^ZfH@TE#L$Ko)^zReHYIT9k+jDVuK%niUP|dT(%8eE&UC| z^zq8&9h<`U)Lzv8?mQH(`GD`{Fn%zTHV7HI-$g==E$ktXb_wt;!@ay<`>MKU3#O!S50k{I{#$#|)uWGML=xO^2C;=?9YyQ5UTEzRA zP;n1*I=;gDQ4LfuHiwJh#q`Y)YGCG|G?F-wL5>xkf_;oJfGWp5@J~qpLsp?sh3nR% z&V}-r5LfLC7~v2A1%^37wW8)gnmeGbdMJs8VGOSI4zaYVcL#TWPtoPdwPR)JJ8wtD zGghAJtVL-<8Wg`5m#$iH4mvY~jV_vgc_KJP3}b}Z1tE-ARxNgD5<{wP+2?#_DiRX+ z(tt5-ki2~Nb@>~pf>xLJ+ot@4dRKS3{n{UR-TihsK-AKqwW%oQ&lbys3~)NAKD#`J z2wl0w@(zA#O$Jb+Be)##YBb7j(`CU~ErLJs z4_Sd#7z?yHUi}s8ys8mE_@7-e!Gu7S&c!zg;UJ}ya9aQ!hz&r+IfdlVv>DLVa7T_- z$Gz4N--vMl{IM;ddI6Yc2nt&^0RXZrq4BQnCJ*mC0Fl`^L{&bzQ{)UOf=Xpt{xxXc zfkFtc9^k$Z=@}_D`E9so?YC9lKu6CN@G)da^kzu86i*#n;{MoY&z1bu@0=PDn3%cP zfpTsea<~$w{{cH;*{qD z*zHWFe8U1r8ntzA*S+Ue({;xhg1kWyDRfD31~VGjOaxoAP8hc*-G{h+D}sLQ{qZ?m zGg7o5HawSrYIJYT%zpW zlngCGmgYp^l*MLzV7dIy!XSYU(#&syN zE6xjVLZxNPuZ=UUYt0hVD@}6SjO9c37`ww=0lbo97TZ}4k};}=V4i+lGbalw@02Cb zJ1j&16%>Ptg9w)KY1;}>QO4rzz)uMM*IyTyPWOITitvkb}^3tW#8oV`;q4Ez;_Twt3M2RJce=VbDhp1n*@?#`(k6=s(z;xr z&^@(naf@?pdA~mx;YlxkT_qX3YeVK`CFuOp8y%RNPG4hMx@_6lSRt=@KypOT7fal{nae-GkSGuv?u>c&7#pVGlHj2H!MS# z&Yn{j^Eg@TeH);QYZ6Y`uKvsKpi~CQ9pfNB-P^y9|578Y$1%Gc_s{jmoqShwQ@9J` zFCzeJh?0dH$ksburx9(T!m8Zbr9X|=vF=7~d;xo{R?pdwDt_|<)^)p-xVi*Ht%n!G z#cZKqp+(D=ivO|>%+AfheqUhaX@dZDmS zXn=qMW&lM;u#lSna*2?yMMIIoJ>_<95HM^aaP#*J z^Kohu;8(niIw)j*@TUzzTFXr4{)72#f;PIx^jdfG-?jnakUzXPDx}rq`tj^G;QXSm zELAxCPb~eLRe!UJxBq{KR;lX>6kf8^8yl%%A}N8vIU!I4qy@@`xj&Rz5sf<1Y#dt4 z*Ew9>YQfLsJ&*MJah5i_!ht;=>W6Ktmw>r3J{xAW)D8{<3QxFLNASbd>*~T_GSt z{fEtbKNKJ;s*pVCb9Aj>Qs?S2V5|BhV8Xc@J++$P0nE5!C9 zSUy{|w^abU&v-(2>c@sa)dPUdINqm$nl7_*Lq*VjDW=pw zJ$=V|306fWU$nSzPBdJ)rM5xikaJO(LKxNGYoxnzP+8~h2SSaP?;+!MT+?=oP#wNMHJfRmSn+toPvoa}r% zp$f^@rlJl}PMrO=JuCOW9bw1IaPl+TX5Y6iksyxy5#wLyLh>(O)UxH(3{3|BU4DX= z!@Lh3EY8pIDIf#~@GA*YW?Pl1@zxC_5yk!T!XF56TxK1IJ-H50UVsq~5E;+bpUREv zX_s$zU!OO7*Z$aaPtSTk6|88fOqew_=JGykjzzGi#!sUCNJj3Ijyr=fLw@X8U?8E?JUuDmlLCiQk6Xtt>~p zxbRkNw8G(m(V0wXXdZ^@anUJp+^%(blwF_dJ}qpyBWZ|qDP7koQEjlOunE$do1T}w zw@#ydaMcrP9l_!`90sAuh7S;7Dm<(rn_>!zN>1xMbSIdg3!4@e)Bz3H=ML66&L z?$wM1VSax1?*|tsW2CmF>|UfzP}5Q2{Id~ADNrM}Zw*gwA8L)W$p}oP@Ztyky=;^x ztfc`02wgUFO@0<%7ke>sv#?*6`B>IuF)`yRY6r_(I>-1BFmiw~woM=iw}w)6!=V+f zRei)uj25cN4uEiaLk(gA4|0k3t4R(0u@LQz)sE@b8xq!G&5FsnkvphP#9% z*B0QKm22ypNt}EdmEha*%5@EjZu}h4h1oSlEMz`xR7D2I=89Spg=kzV%;H)K6~dWb z6HtEU7k=RRP03Hd65UguI_rFInFt7~ZQ~9mcKh426;%l2oXIdcbCki3g$fK!2-vS! zXbI-e>XfbM5-`9KQmHKZV1n9L`@?SZsV{=VFT~Mj^1wWHW(inS!eU}NqrO2>HT153 z3YZ$Q`xraXgTJ=M;&?@VDYCA3C3afp98dHME_6#e$p?Z}iBE*AwTmdE#*$l)ztr(x z;(|{3Vm5SKSeIoos>9s6`%2+@X+xx+F-U|gAoqB~^JaT-k*xa#!}8-rh4`3zi`H`e z7OCU2iO}Q)d766A&VngY<=enymf_CAyuIYKD%<@DX#gxh&yPk;8jI7uy}%vZlVJWF zVHp&f)K;U0orYZFE}uNrfSgh^aE!=7tvkSB?=iO58-fm>d({sQM;#s$hjmOSDpJ zqZ&w%@a1}beTnV*OqtL|0( zsb;gBX(|`P+V7N(3+S!t13wF=tR+++#6NW7vc@lj?TJW5K%K;>bZfHQr2bp4n;cs~_+Xv=|9slbV2SC!bpEIJ6*548zI z_8wOTqRq}75$Jq5)+3xHM>N%iPA1Shs6gX`98;=M5~@u7h(L7AXvyZ@>|Ndq1)iU1 zb)iCzgn9iHM|1_^ynvAAM#GmCYorUU~dMN~@rq@&1iHEgB=>{wsRfu|%#6a`z{=pU; zsk)jk*B5CctLo}% zV+3(O=dIy9EE;~5g zx;;qP>Xj(8l|} ziBu?#MowgAmsS|aSt~xDD?&(=0T|&|a47yt-shqcC0`yuDuT%-pap%_W^b((ZXRg; zcxQAzRDGk-gtey-P~Dc_SqahlNhr~T9j7#)9#mO7KRFwy!7jF)UpjSJkU?H`*r!{l zHHA}g7eq{Ul!yJ=Z}c&d`T2^)4>B?w>_vNktBOlNbI~M52+;&b1lV{;88ed*)+O%7 z(w9+lb`_+&KH?VQcdoE+NsujEr5K8C%4DZY6FmR=5fiTs4rRQcGjwX=TOQltW^3pbz`MN;Sv>-2wGQdtlj0B z6@VjZWPnEU2sHo!8XL>I|~D(Kl(?=nv}?}w}H zUwDtE-(<|gVi(wKmvx^W62U;zOKm-ZGm1bI!3?+Y3mPI7A!HZxVn@`*ro5NE(fwKL z6cW>;b1&VETk6zreI3+`VoK%$CMz=0FtvIYRPg? z!y+_@K(C%eqvrAcgLpKDuubq$IqY6C{?vN?T~?Eqe$S+m6iC?jC*HYk<>AZpEG%UFyZdoL+p@uIN?@5-+ zG2?h1 zQ0LV73#Xg|+`4|hw%%d)j}NZ}j+UHALs-f%2DAvOsJ*|lcq2@G6n#)Pb=;1BtlqNM z3MPM?e2kZ=@QQ<)_F~NShJP@N>TrfbG545VS1;YktW?EN z)#?Xr9pFa#K1zwdS5+|FJ`7;W4@#DxO0WA^nKsb0j4yqWd`|x7>ne-VGwccCc4)){ zKnaO5g((Q11rX9Kc=NSu@wN@8+Ty??ST(aCIEAYy#Y9vd=3^|+$aV#gm5506AW-~h zq#TB34v#v?viD%eOyQ27c3LCgDwsM8snvi(ocze3=qGsv zZI$~45DFXZPn-W|SVfUdshRWXzkL=uEyK|{gLNor#tP25qk>k8{nf25DG*k*u!DRZYOD&u(TlOX%(Cic5J-uts?XT zU@ALvr<2)cdh!G$a{%~b$!DgPHjZpmd`G0|5IZqk=p?KLpL6ih@6(aivN#(eb+$Vr zo?`g5RgWrB{FS*;ehFIpY=#JJ1D+4TL%?+z@*Fezj7LMd>Kk{96%S`pO3hcEEd&e( zj7@^9SLAO8&~?<~35l~_|7n(LXqz*PA2Azpc1@M_dj=pIJ(w#*Fh)I7U19t#Ee#?$-dVyddD@V?b&Ejr(C1Dqr1 zUbv797>o5BtbKM7ui9%M=o=OQCUP6=l*B6a-7cgnK;lQh_JWC`dmJ9gpH(eX)2#f@G17fUSq7g~+Qa?74FN^wfpm_a`q<+Kx4Td6PR^ zC*^X}uP<)BftE*w%!@S0b$Lt^d`e+s3(@I|@#Hh_yjsb$<3va9WG;X~?a8 z%%hg{*=nEMXLGf`(X#7pSzgUfNP9&-KTNgom3R#RA1_cWh2R;L9IYy^xMs1{%;LcH zE;OzOWXm&ny#kOAf(}W$t|B|3M#|Utz*H?^aSFi8Tc^Hgeh3T-lCKV18=3bSA~{Gm z=lzQVMN5oT@@kpSbb4QxsXP4Dikt&TX|K5`V^52W2g-WgI`q3UyqUa(37PMckk@cR zE+NAE@v{#DyF)*j2o@gR*Rd^ie2M9Wnlz#C*5-|WdPJ<@e{#DX=lB-_1d;q)JF&!$ z?@?X1Z!x96Vvl#^R5hUJJ1|Bc*y)EEFkvGNMNjrA>}NYdbp;F8?HhtOoD75hzA#c_ zv|szFx+IemQg6rSar>weA)5&hQQd$svVq?=&?ilR z={v82$~^$&9ON?Qty3(RiaU(VK4qTj$0Z8>yAR5K6m_}Vr=d zf7ZVK(WAxiWp?f}_m0SyX;QT6dT9JX?~l9}4ZtUztO!iw_(-`dscqvA;h*NDwuf3r z91htdXJ(dGm;Q=(h#)PU6C(L+Cm?<1zs&2nQkqY*!Q&(DCo3f1&DFUc%Wygc;}qhd z`F#NJp9s=p@R7|KXF?+dN-vQ(D=yrIGeSJF7cJsk|>qDiy}31$7_U zS=m~f{%VW`6PHVFLM91rVE{Ki66c80({Xh$@^jD0yzL>-<5I($?`68rXUf@uo&~VN z(p7hLuFu|uZ*z4QgONgx>6Jk%KY5(HAma0^SAz5*(D5Longls|?a&s1s8n7w%55B5 z(4wsbrboW=&M})miAlP@PK)r)^qlI$j3@h~OgF0-#`;--Kpw@P8wpDSUHTJs3mo}t zch>gcGS*%5a9z3tdfGOI$?E1M^r+}j7Z(A0$hgHQ82-ACg@UCopkFufZn(f%QUBrY z&%ul*^uQsba&wWZw@Gw`)VdmT;J9`^io@D1vtP%$A5%bm)V;0m{Pie zB|gwD^2f3rO}nw2yxr#tzCbKDulN0lLxldg4T~;B1ptc#+gd*8zlKmjPvOpO6{;2&}>pq@aoIo<3DnSKpGiqr*qn4kYlR@3s+asPn z0<~D;@KhRd+cDVXGvWd3ViEGujF%eTJjVXAS6YP6y(6X{cG14x<#k!d8|bG0?%ka3 zr_U$m$sF&AAJlv>thsZiLY&fr26d8}n(+h0U;8L^*;Pwp?^YmuU z5S@SFA7|(!_^6{t!73QJx6W3Tfqu-+nuY!TS^Jb8^x{Y#?Eg^X5?GkA4)j5=pJW>H zW`$YT{aK-V_BPm;hkdG?%t}wv0syMh@lwy|xdTK_exYQTO;(0tz;8z3yBW9y2b!M`hxz#-guD=rj>ciZuphS%Fsx#h*^XkuKv8(U?|OJW63A0kCjB)r3)f245gMR$0e+YR8C z&jvIUVuGs^^JfM_B=i3zhoz_G|K{-D9R6E}|F#nUjUE1fY$X=I?Ne8RPNA7@+r_5j z;KBp(>Catm?R!^E(}cNoa<~^SN~~{9O5*fy+DyK`9csdKrULRr`X`1#kRO0-(08M7 ziDEetEmQy%LH=gQt0lz_hJ0BqfHG-?GZf}v6ks9*oTQqt#fkAP3?p-Q3e{0$Y`_c) zAJjtkNVtRF(bMiP&D%bJE|wPOc1b7(r$NWMG=O9-XRLGF@eU>lp%P|H6~dLEZxne^;PYWzmllDu$O)y9`qdX3q$5RZk5* z1ft7mTU_rRkLg`{5n`357QWfdvYyauFmnY(gJ|m9wx9v^5J=IUy0!_qh7v+qC{8|H z(l@cv6(S47g~!h(xOap~VPqo|AB@I~;G$_%hy!pVgrKLjj&>tf=b^ykT;@@BE}Dl! z*yt{Nw=On7xFNU$%y{oLOz*G5Rb%1X3NIjjnjJbVwF&^lvtW*y7EIKo2(!yP7dniY zLjxS+MpyVe(=o&8_mu)zw%N-@Z zWUd69z%XQS0C6NNqvu0b1r2bmK5k>(M^#d7d9K3l$&E{hPk#jsH3^_1_iH6};Y$U) zv>Y1#^ZZ^I==@tAZm~qFG9f<-58atfFpf&)@`o~*@hZ5kr`u=ihjM!GSsEc)pvnZ{ zKw!{h!PHCdK}rAcJ=F%SxET!zJ`6w~1bRUq4!9r7cubld8a@6mpAL7D8Bp@h%HvrD zfH7tm;I%KG3V}KLS!019pO*5U#r9@_)}8nfKDdJh{Wij6Zei#q)B#YX9|C1$%vLyv zSDHHZejEUlV2QcoDw{$;`CQh{W5YKrVj1u3{g|3H(3g0;YLl1A zmptsgRQ17?^B}(ms}n}~de((dksr3Y%?K5eyv8tntsZ7M?Y_VC&^kUBQbfaOJ~-AK z+D;G*=zJCwS%3(S>3<@EmWYQh93aSdW3AKPdl$Ye4r-9Z6yKx=70{t6S*Th+U*8%8 z^AP|h$cHaJL3Z^sN;ATN90strCCub(?2yFFcWfQBR(MIB>VBFfxTehOAQ+adt@pwlbfZ?*OV^wrOi zIt`!QfW|C1Wd5AWM|v^1(6(2ozZ8a@1YF3JM#na?Fc$^*jQ*;-QI%_XER&&zP?mAr+)|!Pzy%n! z{h6g^9u4CliuK4ca{ad)#WNe6^+q$a!~@143mm=&CYQrAeYc9CGRZ#da9ZaVclWgM zxPYRtc6ls>>Ut4$6x(YN&Y;*HO}&K#fB7944l@h;N#ZdHHD4)!0jRrE6i&sE9_tx0GgK&j&tHIq!8AX4pt8wLT`-I))hqhQ1yOh*%5#4 z9M=zHj3L&UbifX+3)2z3>Ui_7qO${11c_4e!iBxrX<}C{B2F~RRM5!MTWK zkTj53VG<0q3Zl=a5u0DN3gUWLv9ZKS#S=xF(&^UebKkyx1anNr6=75aXqR&GgfrE-nG8U?*$UOh6pw)%GjxZeq`~)mh5I~2CVjTX z)dz9FDRS}*r|zOm6)3q~zd)bBKDj#6NeJ^rA7@a(hZqahYdPiv43B(g(5gOlt)2l0 z+qeJjNkeuEyadk<{Q(3)U{`ID7y8gvmIj&(W6g`f4tI1y7wKx?Fbq_kD@=nicNS}u z?P{h9RvvyoAD#6Qp$?eoCJD?hz(AEhSe(4YZ|j1_Cj!+W?Kxm{jPO#pwSSyAp?*CV zG8tv}aE{<5&_Qmc_v(40&-Vbp`!A>nji8e8zYPDc_P#tG>imB^)5tJlNG2^A_mQh; zO(Zkpu3Tj+#iE3c{hBQ+p~j36Qc`p{&_xU}3-+#W3Uw>_D-ZSs#>v>gQ_cEjUHWF$oz(tv5B5`Sv~zRR*FMiXa(k5unv> z=)zJ0d#X$(3;b-5)Ni_J0xvS&?PW_f$MxI%{G51$o(fZJC3H8qs#5*IS!OuILQCye zJENnWkNH&{Ssd;8L34pzCH9ge#`Wovbl4Xa^y}0x+67U=>40%iNZf++n*eqjUnA2{ zKNh5H=#EO^o8etB2_Eg7G^g+mISC{&s60HAgvLCDc4-5jh&L7AXxWVBo4QzoZPX>^ zvnWlRCGdNwBhRR30&U_7H%_J@&lT!8FhTR@=dw5NqI0(RaFO?OE|g&)t(KPA>#JWe zQ548ISB@i^GKFTLH2L`K2A&>thBT#8hu?}Wy;Txj7pdM-%Y5^u7S%z9Kw1!+Xz+F4 zhA+stx?&~lCu)2%@=61`nOK%L=EICAu@ZNGeM|_~>~V|Fo*jEFq`@KsDXe&EDq3v# zV-}lO>zqL@Y8x3D!@sM%w^g1WzyES$WafK^n;(6mr%N8dZG)z_yj^WuNN8)2shN2J zE8E|q>c(af%ws+@zF4aD)azV&ozl&Z^{N@OHVXO?<2I*$yChPU=tYjH9xz-YbCmVH zsWUnp;i_Qtrz?~15Nn^AHTh1W#0h%fWTU7V2i2nH4a-CbTvtFg2I#c+Gf zP^<||bps)u18F@TCTZv+#LM&>+MVs?3CrB|5TOik#RZ> z2Qf|jEztB$Q>w!7+hq#?JM#0-QvFzSoGE_X)Ri4j=$+&HPm+bpD+8Z>u#>U4cTO{WP^A!P4Ea z^kmc$6inHz3l2;F1x)%EF#rF+X5tuf+Wi+-Fi&={g){2xl!KL|@u>jr=UxC4IfFNP zvaYbf9P2GSWg9auR5UNac0x2fKe=p{N{LdRHBh z$NS8Dh_~vOH-hU5a9JIu>CMgjgNPO#BJH%e7{&&`YtpMxV zVfr4D8&WON8gLnwG{H(8uolqpc8Li=5xV{Xy|`gJR9mVM0ud9^%v*Ge4)!OAv!@ zOoG8PkYn03ALs$=l;ZPg-`akIo*TJ!(8)Sk%SF-QiU`S#Vy1;tsh6Hdcz)10^g1r* z{oQnj^XJ=(8Nb@Bz$k3hBdKnqZN0u|x=Sz4H+tkn=6gT4)C!^Pxg8_s-*ttB8*{x` z%dm3EhCzB76CT2>#Z>o`6l~-;LwR8slm4}i4+*<;_r?yI`9$CA$1y4Lw9R_gX2*3YWAx>4f{llxu4&&vk}w~>pnHaJyP z*e^rt&mOZ~&bqLSiJRy{jF$Fe1pC&NQg9Y+c|~zS{Y_PuBsA37SCz@~M&O?(cZl(rV>?e3M~FgJlM@Nh6603Ds5- z0={BxWM;?IT7$tiQLl%6ko_5hO~@UksI1-N`New$jfxW_I^Ao9wKNLaRa|5wR+G;` zibHsKcCmo~qrrDhtyRv*wnXw5u%ih#w{`m)1|nYUWroN!@%17t0`mtb2KadAKNPfL&He#}1WKr=C5LH3XwGNj*%U2Tne8D* zyJk2`31igyJHB+?Q!W9-*VEzeKAvk2grB(B zF};CfndyrUKFgD0#-bVl1ufWt-wwy`n#ppQ?@QvkN)vHR#|)+0eAx=2D`L*P&Kr%FRYp zd5jdQsR#gC%Q0So6A1&{owVRCh0IYjAz)5e80^bdi5w5zL#db2xFRW+Pn&di04}Wh zA|U^%fY5~QY18+gEz&BY#o#VgF}64knJaWSQbzCJHhmGRS>((sdpSQXMZVVLO;HGU zq559pS0vN5tPY1>#`oI~8(}Q9DQvG^4=a6PMp?@?yyj5FHqJ}7ULN~EG7M_js1x_E-lw&Q;uI)_XX6+(W7S34r z?K_PN!6u~qQq2$ie)V8Mo)^jP>@Ia8Kd0z}7VibX-`3 zbywZlObyNVXdDSI?)1iO2CU#|#GwWnyo@uZIGCcS9U`tW9QJ#L;8MW2gN0!8L~ zkLt0r_jMoc%#*SC_qSGOj3_nb^$k4{fV`u`UaE(vkvSorz_k;2+VG}kc zJ8-lya!k3^tV5}iZ^vUJOX8keKtttQ__9t;(!Q7v4s78tc9&mA+I zNTd(6ZoaHSze95w=%B)xsaP*t=XBwQ9=qPxN=};`C?o)vVW|60$6Ff|LLADmgw|>R z&w?o@8)>u9(jJJ--!gJT;e#`rtu_*rTeyN0^~4!QFrIG`IsPA#&hLBC{}A3l$U&mx z8yFcIQiE150wP-8Z}n0j;0PK|I1H*x6CDtd69>d0DY-Ed#mq@6RET`>(swvmg>7nx zB(DbiE(KCsnOS(hJhH5Y_>&)P9vP(v?=!b8N1By+zFwkL-WKjyRg79!vq{E14bq3& z>{e}s2m(Q-pa}no%vxULs8j76Bv*%>vft>r_P=x67y~t+wG~!!ZYxWY!hdZmiy9xn z<+y0_6#A?AU$?En0Lx?IgAypF`@29T+Vxhc=Y6C$GzR~LGe2%x@lVJ5b%kA72(dzV z(1Ne6z>^O1=fB@ws)2$1NtKZ&PgFahXM3vk7tv_tK>JtIB8hlP{7=P6y5Hk)mGlr` zr)4P157O$T0*K!e9^)1_i;j)|58(}rvL{$zFHw77hDB;R5D{A2@{h$(K)H0y;5UXG zoezjGMv0S6m)Y={)%Y*^hWFb(B1L-&4>opcEAUZLGqWGQnHi)ZwoaA6FQS<(~vjX`Vw%9&Zmr2px7Csm$1lz{`3%hXMr=gONO`>lK3 zLIpzfiisyt`><;jQC?g2SC!H=v-f7m1soY`Ri5cW3yz?L&hEm$7c*~e9t8g)w<}_a4-w0V4ksZEN~@SjJ0vp?4BWam~g`s%MJJh3;3{`1cn=63hR(PgMcEGlcn<1wl(*{Vn1_KGwQ2z zqeF;pRzEQG^&UMuXuDao9C2G``4IhmMUxd6DQX4^=m52YWggp&r>4SR77x+T|AT`c zMU}+dF~nejW7jo<*qZe7_je60-ijI|-#uzJCHP<>+=c_Exi@c|NcjB}9>VK0TSVvf z^B%oa%zqJGxc<9y&vQHCq6rpKa+1L?RO$EOe)=~8rsQ}&}39(TQM@+a# zo#rcJY5`q&)d7!cmm`?IcJwWG`&V1^g_BZ(UR+m=M;-PG%cp*^jt9 z7iQhrxGf^d#)5R}X#TwQsO$hv!0&9R;?)He!hs+yvYO;WI=f)VZaWSKG)=|C8{@Cf z;Lp1Q4qYYA{tMPVqk&RNF$4vd# zHp#LqCAXsdOd#XH&jWzfzD98YSDhLp1ehBZA?nX*l+s4&3?=eD?}%Q%>gWs=Y^Y2T zsyk3tMuEax?K=v%fSgR){94H!yZLKN;$rCOi!2Q29XCfCs$zr|=D02;e`)YE{>o|a zErx*<8-K5jcQ^!iX{BDs#_pnBf-?+>@!dg$a)W9`a1Fi?=nH|qNYMYg1cB{jt-yN1 zR%{84ehoSKRWhnrKO9Il&OGno1Ld|rl^-T{MiSaCsU%|?_UK)CasL1jko z+5O1ygO!($@5hme>zIjKyawbfs#pQavKs0c zH5wk9t-W7?_Y4gpdSBgfVaE`TZC2%~Q?aRY=LKYv^B%6eYzg%_j$je<=H0RDcXBO} z3gI&gmqso2E&8PscEtB4PwllU7HULnB5C#HRcqLgCGEf)k){Ycv)wPd^9wGI_OKlnjckVeEH5xu^z7feUSJ=PN zBLQOc_z4ok)5+MepkBWN_KZp7WrBY*oOIbhjX?#=6!JX7VwD@@oZ1|>(gufZi!F}R zy);v`mr`mQ_Fr&%OQd%kJW+~L8fxTY{kNcIIN+$oKo3v|mnRjBcijl;`jJV06C+f# z>u_?DuFuuDoW)Y_Oi4dC(j$t%YQ>s?>0#Ps8098gZIEXz7aP)ZzNngi^ih@LTDlc~ z?mRi{#_$dsWjiFg2p&G30)}e>vOlQ9;Wl^J7T+1E6oOpnygGHJUCmIM4OL!P+X_D| z1_`xP@fO~WzsspnB@Zz#lDc`(ow-u!x9ay-pV-xVMm6pc(?b>YkIeNYLuCTlT;K$$0)Jd&$bxuES6F$c3HsqDt}(gwJ!%#2 zM0`|n9?hpX*1It6BIzjxUH=!zE6cPk?*Lt2wr6dqlZup=TxF4PHvB=+&8x9_#QMPmRk9R^@80aJrRyIuy?^0wm($TYauhN)V_l6{3O5d`Ltyi0d9G zHfyt2Gu30~Quza#-)vT2eBM&sV(k7tlie9g6DgMCL6!9VE*$&%c&MIVxJ)`Lk!I7R zy=ffON{H)jO4K#_rN!ran#vD74yYvf41{dt$EJ9)4F5QlhudVI`ZmYjtnY&ZhCZ+q z?vIkxx{}d=MrGB$M2Ot+H-&T-sr&Zh5-pcwFlG=i0Qj6Tyi4H-ty?8ZHE#zH|u8Q%SxB&t^;M zW0?(OL0xNpG4sP+3$rljr`GFJ7taN3&*OMleZ8phf5f?cs3=ZSH3(%AHBSg5LVEB4 z_-0u&`R%K(ww|0y=))e7RTP&G@{+u~4}1A{MZ*z_@8s=Kb$wkZm|ME{ zjO_wxaRH^x;qukBhGwB_Lwb@recAvI8linn(Sn}SUqg~>wiDU3_#SX!Bw^(`kGQ$;FkC}lq5+cXc8>~Y%!iGxs`;7aELoOU z_o6iqa$C7ajUhwyKgD;0$R(K_DwNLl4X|D*l78%I+JcgIGUv82T>!&QYJK>cGs(J= zHaHcXLfTwBRYpjg*9&EZ>|I_Wm4nJplTDX+%F4G1hR}#O#k1}#WK4=;>SS5E(NLw9N~ZuUQIIo{_PFQm~g=DJYjX^zsD|D;e>cXAjZDrXo!XU5%ni+5D2i-i=p zSbaS%(t#W|+@6l*JuDwc0DM!l{BYapK_rNlM`#0Lg{e2XGi9z$x~mWp;l@3cAJ`RU5BLUM!8RtFqjjEx*R_&)eMUr;b}6GrXUW-0BI|M{=G`Ik>Uvy<3Ixa>rS1F6%V(C2j6ykByYx zboo|pNRWq%&vS$1;6zS;x_}s0eMFHWiP>*AB{_qPqxmm zC#9fdA+C#u{cXE`Ghz(My@gdz+J0nj<3Z4C6~Zi6?TG>Q>ZszW+~jp~drtqnR42vM z@z<&~J{@s(urFRPctNLojg23o~n(Knapa$|lc322(=-%8i z_Uv1KGAsmpNOaqbFGG3ai1t~Dne?fBYgW6v%b#HkJs>?$bJZji&fvGxP159Gj~lx< z>TJw-m_{xdN%r9NuEaqtU^!Bp$X=Ui2D0IS25TNyES?f11Urm->IkB955QQXkt&p$ z-CFm|!>E1$Y zHW_p1MecKEr4X)Vl;kezB{gm>6U~hN?ZJl4P+-U6QlHd{gGr>*o|ffyz8FQn(`@V3 zajYRSZvEl4G}{-&fI{=IH>SCLJgztia_J*Yi0KiS6X~Ix_WXLE@XZYc=4XThgPEj3 z#!>|>&PNOOWm|8U`sLs%sJcJ2C!0b3P&nQ23 z7D#+LS};S>8AL;nPs}h~jHv&BBkrZj7k;yJZvv%q2?lwzGfoVh!IfZWKU5PvaQPhv zk#)NrpA34@JYPS;ns&l_KKqM9XI}b3&0&p{?Cl`kcfHm#O#X-{K&$L8Sl?N52 zGwpHf1l`B&dev|B4?QqcSC#(z6M#S`vfVVX7wPn$v&T+uo0!~rMm`z)nS59OL_Rd< zoM}(fQ($c*lizv>os1)=Z!}0bVLoe%r@w29xoobRW_Bu$YM{!6kTF-eVs>k9td>Lo zlv;t^&G!PkPecm_H34ssE@tSR;=8bUN^;GR3GhtnI&yWa=2T1(9u@Z5bV#fmimarE zpAu^aNE`f)5ksWVA=GxWu-lpPNyr;B{v_lOeZF?l4pY>CS#{=eXBdxjep0^@P1hz3 z)@HA;hU`m|BK^VzQ?74Mk~m>IqOE9^NJy)#QHgJ`-P8KweXmXGr*=3`Nr5A*rt7!n;IHZw%>!0_`xN6T zmHpKb!i$q5eokChMJCPz8*}BQx}Y)V>`L-+UVP1UgmF^YqSNH=Bk6A=?znFjsgqY0 z=r;8YKoO^K=G=T}rIC>k$Z2uCmtP$oRe=@_=v5xdDdTp>H6W(j>$w>goC3Qm^K-}i zL;x-a4m#P24y_)b&_88OZT+6K$m z$^0#&4)H%7p!e}{_SM=Rn>kVG8SD6Uj}MM4(@7bLUvdBshN(g+?dFD&)!Q#VGpK-4 zL}Y2fqUom!qta=fqU3Nl{r-S=Q?nCBf|vVNR^+i`XQ7f**8nj3Kbl>ehozVaC+APnm`lUuHJOd;P$)g zP5!9Wy-5GctlALQgJ0J?vR$AQfFkx#}2DG8j9KV zgUo70$T{xIn~p7#d?%M`8aa-(zN{Ll_)eL>6+HG$pd{wFGbwr1-NY8rWZcs4&eh1|LI{b5u=mV_c*U}joLQo zKR8MHyF=a*<{m2RH_?kg%U#dHI@VM{QD|drHl*}E5sG*9>#3EifJ&D}uFZyYnQ*N) zltm2ZfYUoiBfI#`RWBL)nWI|HawjO*4VYRr*o;-8(NBfi*iA6XlBN@AW-&vKQ(%dx zui^`aRJkgv#Q2u9tKW0Ldb&Vyq6pbOHAe9_sj99gwoF4*p-xg{tExc7_7A#wcE z?^oG+@&n80!)D@)8}alVm9>ifAQt>1*oW7|ED3RJ@Yy?=vzw$NJCGltUN`P;juWP! zI+U?mCu7K+>J1JZ5j!VD8<+o$5S^P2ME=5DhI2H65?CEB_^Hel>u5*l?}7_JpRkTR zQvQzx6sm>o0pl|?-kgA1%n2w<<8R;q<%~Y-C9*{kvYKqZ2dn81h3cazPx`_q-F5l{ zke`k)a(EBJ1up}hLz;ee4&{XQCSJwB;^)<${(=Nm|baAxtWV(B0GeKZEe<*A#r+ebq(Is z_SkQI2vh68p0<4#KxfDfEp)v0nMmZ~#-YJ15NlEBs~17cL$>$ow?}kcL9Lu${uI{( zSb&&7Cfl-JwkIBbkNW_%1%{Kgp3+3o`1EG7XH4N!xK!d*1 zSCBPY3JS^L;5Gn#>X)t=xInpR*SJy|T5ed4D!WC1OOKMLiN4climCSV!{CbFPFcNb z_t)<6o28CUYB~SiA-@xW9HAPzaAxmz=n~+eO@XCQ2*DSLF$D0Ru%?b ze7ojr%|RMJIFdfWXNiuHhk1g0?&In_l!+VVxRa6t+1Eb2fQ*@)sLMnZY z)ZwRhM{e>65_WT6onh1{G!Z1yD7~`MBOV?1?2CB|2E*mo? z=cs7LC_?jCfU$F4-S!Sz##R6GlDX0P>SOr>uGI8i=x%`evJP`qsn758MHp0F|IwcnxfZu%+YCFfrvZL!c`IeHq z&dN7;`hLcamngGHS-VYOyidb*vsoj{b8VXZq4tS7Wf&i=MLVt;*w! z^B&3^^)_pWAJYiU=z>L^YstZ|C1;K>9>RmaquRU14DTrPSli>;&S9;5B=v6a+6+E9 z2Uu8OA?;@eE?|H%B1D5$+RTK1=%7H$xj~`@j9o{>uUW kMAI)6KpC)<%tRy}L@sSz@FNfIDb1Vb;^1a~jpon#Z + + + + + + + + + + + \ 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 d9317ccd..f16ca7d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -497,6 +497,7 @@ account Log out Log in + Auth Locally Switch account Add account Create account @@ -562,6 +563,7 @@ SDR Web Poster Image + QR Code Image Player Resolution and title Title @@ -780,4 +782,8 @@ Media Reset CloudStream Wiki + Visit %s on your smartphone or computer and enter the above code + Can\'t get the device PIN code, try local authentication + PIN code is now expired ! + Code expires in %1$dm %2$ds \ No newline at end of file From c71d5d8add602ba0be84ddf197da1918c6f65970 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 19 Jun 2024 20:06:40 +0530 Subject: [PATCH 091/157] feat(ui): new dialog on adding repository and auto redirection (#1025) --- .../lagradost/cloudstream3/MainActivity.kt | 5 +- .../ui/account/AccountSelectActivity.kt | 3 +- .../ui/settings/SettingsAccount.kt | 4 +- .../settings/extensions/ExtensionsFragment.kt | 6 +-- .../ui/settings/extensions/PluginsFragment.kt | 9 +++- .../lagradost/cloudstream3/utils/AppUtils.kt | 50 ++++++++++++------- .../utils/BiometricAuthenticator.kt | 10 ++-- app/src/main/res/menu/repository.xml | 3 +- .../main/res/navigation/mobile_navigation.xml | 21 ++++++++ app/src/main/res/values/strings.xml | 12 +++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 11 files changed, 84 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index cc2c99de..8d312ceb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -134,7 +134,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup -import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled @@ -186,8 +186,7 @@ import kotlin.system.exitProcess //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 -class MainActivity : AppCompatActivity(), ColorPickerDialogListener, - BiometricAuthenticator.BiometricAuthCallback { +class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback { companion object { const val VLC_PACKAGE = "org.videolan.vlc" const val MPV_PACKAGE = "is.xyz.mpv" 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 index 0b0d83db..0da69f9c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled @@ -33,7 +34,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback { +class AccountSelectActivity : AppCompatActivity(), BiometricCallback { lateinit var viewModel: AccountViewModel diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index d227f9f6..67a2a15b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -51,7 +51,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils -import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock @@ -68,7 +68,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import qrcode.QRCode import java.io.ByteArrayOutputStream -class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { +class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { companion object { /** Used by nginx plugin too */ fun showLoginInfo( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index ebd3260f..1364c376 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -33,7 +33,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog +import com.lagradost.cloudstream3.utils.AppUtils.addRepositoryDialog import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -273,9 +273,9 @@ class ExtensionsFragment : Fragment() { if (plugins.isNullOrEmpty()) { showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG) } else { - this@ExtensionsFragment.activity?.downloadAllPluginsDialog( + this@ExtensionsFragment.activity?.addRepositoryDialog( + fixedName, url, - fixedName ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index acfbc584..3bdcb251 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding @@ -70,6 +71,8 @@ class PluginsFragment : Fragment() { val name = arguments?.getString(PLUGINS_BUNDLE_NAME) val url = arguments?.getString(PLUGINS_BUNDLE_URL) val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true + // download all extensions button + val downloadAllButton = binding?.settingsToolbar?.menu?.findItem(R.id.download_all) if (url == null || name == null) { activity?.onBackPressedDispatcher?.onBackPressed() @@ -171,7 +174,7 @@ class PluginsFragment : Fragment() { if (isLocal) { // No download button and no categories on local - binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false + downloadAllButton?.isVisible = false binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false pluginViewModel.updatePluginListLocal() @@ -179,6 +182,10 @@ class PluginsFragment : Fragment() { } else { pluginViewModel.updatePluginList(context, url) binding?.tvtypesChipsScroll?.root?.isVisible = true + // not needed for users but may be useful for devs + downloadAllButton?.isVisible = BuildConfig.DEBUG + + bindChips( binding?.tvtypesChipsScroll?.tvtypesChips, 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 ff27b192..626eca12 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -62,7 +62,8 @@ import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.settings.Globals -import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll +import com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment +import com.lagradost.cloudstream3.ui.settings.extensions.PluginsFragment import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -386,7 +387,7 @@ object AppUtils { ) } afterRepositoryLoadedEvent.invoke(true) - downloadAllPluginsDialog(url, repo.name) + addRepositoryDialog(repo.name, url) } } @@ -429,25 +430,36 @@ object AppUtils { } } + fun Activity.addRepositoryDialog( + repositoryName: String, + repositoryURL: String, + ) { + val repos = RepositoryManager.getRepositories() - fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) { - runOnUiThread { - val context = this - val builder: AlertDialog.Builder = AlertDialog.Builder(this) - builder.setTitle( - repositoryName - ) - builder.setMessage( - R.string.download_all_plugins_from_repo - ) - builder.apply { - setPositiveButton(R.string.download) { _, _ -> - downloadAll(context, repositoryUrl, null) - } - - setNegativeButton(R.string.no) { _, _ -> } + // navigate to newly added repository on pressing Open Repository + fun openAddedRepo() { + if (repos.isNotEmpty()) { + navigate( + R.id.global_to_navigation_settings_plugins, + PluginsFragment.newInstance( + repositoryName, + repositoryURL, + false, + ) + ) + } + } + + runOnUiThread { + AlertDialog.Builder(this).apply { + setTitle(repositoryName) + setMessage(R.string.download_all_plugins_from_repo) + setPositiveButton(R.string.open_downloaded_repo) { _, _ -> + openAddedRepo() + } + setNegativeButton(R.string.dismiss, null) + show().setDefaultFocus() } - builder.show().setDefaultFocus() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index c57600ee..45acbab4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -26,7 +26,7 @@ object BiometricAuthenticator { private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricAuthCallback? = null // listen to authentication success + var authCallback: BiometricCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { val executor = ContextCompat.getMainExecutor(activity) @@ -141,14 +141,14 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback if (isBiometricHardWareAvailable()) { - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } } else { if (deviceHasPasswordPinLock(activity)) { - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback authenticationDialog(activity, R.string.password_pin_authentication_title, true) promptInfo?.let { biometricPrompt?.authenticate(it) } @@ -165,7 +165,7 @@ object BiometricAuthenticator { } } - interface BiometricAuthCallback { + interface BiometricCallback { fun onAuthenticationSuccess() fun onAuthenticationError() } diff --git a/app/src/main/res/menu/repository.xml b/app/src/main/res/menu/repository.xml index be99b1a8..7aa1f200 100644 --- a/app/src/main/res/menu/repository.xml +++ b/app/src/main/res/menu/repository.xml @@ -21,5 +21,6 @@ android:id="@+id/download_all" android:icon="@drawable/netflix_download" android:title="@string/batch_download" - app:showAsAction="collapseActionView|ifRoom" /> + app:showAsAction="collapseActionView|ifRoom" + android:visible="false"/> \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index d0df339b..fafb6968 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -111,6 +111,27 @@ app:argType="boolean" /> + + + + + + Livestream NSFW Video + Music + Audio Book + Media Source error Remote error Renderer error @@ -617,7 +620,7 @@ View community repositories Public list Uppercase all subtitles - Download all plugins from this repository? + Warning: CloudStream 3 does not take any responsibility for using third-party extensions and does not provide any support for them! %s (Disabled) Tracks Audio tracks @@ -668,6 +671,8 @@ Yes No OK + Dismiss + Open repository Disable Battery optimization To ensure uninterrupted downloads and notifications for subscribed TV shows, CloudStream needs permission to run in background. By pressing "OK", you\'ll be directed to App info. @@ -775,11 +780,8 @@ Password/PIN Authentication Biometric authentication is not supported on this device Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password. - This screen was closed due to multiple failed attempts. Please restart the application. + After a few failed attempts, the prompt will close. Simply restart the app to try again. Your CloudStream data has been backed up now. Although the possibility of this is very low, all devices can behave differently. In the rare case, that you get locked out from accessing the app, clear the app data completely and restore from a backup. We are very sorry for any inconvenience arising from this. - Music - Audio Book - Media Reset CloudStream Wiki Visit %s on your smartphone or computer and enter the above code diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc2d0f86..2968a1b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From b9746c2b17be02f2d33c12d20907cb9301a8815c Mon Sep 17 00:00:00 2001 From: "imgbot[bot]" <31301654+imgbot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:03:55 +0200 Subject: [PATCH 092/157] [ImgBot] Optimize images (#1144) /app/src/main/res/drawable/example_qr.png -- 45.27kb -> 1.28kb (97.17%) Signed-off-by: ImgBotApp Co-authored-by: ImgBotApp --- app/src/main/res/drawable/example_qr.png | Bin 46354 -> 1313 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/res/drawable/example_qr.png b/app/src/main/res/drawable/example_qr.png index 18decbac49533dcd2890ac8112305b19d05cfa11..764cb9660e4edb5e3957576e4e0f24ed3b412d6d 100644 GIT binary patch literal 1313 zcmeAS@N?(olHy`uVBq!ia0y~yVEzxnj6eZ~eU*;CffP%+qpu?a!^VE@KZ&eBzCyA` zkS_y6l^O#>Lkk1LFQ8Dv3kHT#0|tgy2@DKYGZ+}e3+C(!v;j&mC3(BMFfiWj5?%u2 zv6p!Iy0X7u6Xw$p*=CmM3^ZHQ)5S5Q;?~={j(N8Y1XwR{?s;*^{X<~QmM0mOUHuPI z#TsvNbVyB-@(P=<$l-6ApoE}l-Mu=VlN$fG<$)auG!_O7{?>L*oi%-5U5#CBUES{= zH8bRWy64%8pZWUQ|Nm4gm@*h`(oifFAJVW=Z{uRV$o5NrSzr=SxR> zpnaFoEqmbS5IOt838g7Vnb7sgGo1O`|F`G(oC#~V(6zl|Uh~dy=CApy+TVwsek<4~ zf8z<7{tx$=+pfB63vG3GFYqB6zA=?atMO=t^`LPXFxcWq7i3-p7u$9I5}hUK=Q) z+tZQ7?HT-6ZOREX6D2dggA(rz(N(O--dbN`?PB(N8t3O0H19mv$herX=h?IgpXUko zU2S-emlP+YI-x}y!-S*af&2g6an(31*mri7@iYv1HIwjD9O^iZEs9gx|GcEoD?PS-&EcidXet;Aq~nsMYMnOS+P^OQvi-L^ z#h`qP1c2VC*{4*wcgjIc$@O=eT%CfLk5&61DF89$q&Li2-_a4PEaRMbq(~aw83*_d z#C&dgHsxI1vBx*mRZJ6pilQ07uv>Ui$+5*#)^~d>;&PC3P%)N6S7H5a#+O4|{IZSZ zZY#^6N%r$fgng^JRnM`)-QpD=JDO}m(RbVB_3P0@8S?fs9;*!tnzmqn98gNN#5JNM zC9x#cD!C{XNHG{07@6oAnCTi?h8UVy8Jk-fn`s*uSQ!|^y#8;Cq9HdwB{QuOw+7v9 R?u{VJJzf1=);T3K0RSlGT`~Xw literal 46354 zcmeFa3pmtk_cxv~gJ$H^FvwvhVn_xlr-X5?90nDoCgqfBqm !=O#FGnJfjN;>Qk zwbM?{6~dm*X&0jqB_x$8nRnfz_S617`}zHz|MmR;*L%J1e_z*r_S3by=lfmvTI*hG z-D`c;`W|q1b)17&#?P8HYmT#%oyV+Mq5||k2{HIDM=`N$;QvH+c{tK%o$pi`nl+0& z%h}GxYwyP4XRmiEudie?UVl-4qD`-GBW|E?e9W5qQTa>mqQgKmZV{6RXm#bkj4%CnK)EzRLly=+DLiP1^x^Y7~ z?X&uR6@BxZDRJeVS5`-|jQML=AN;zd66g*(f>7;+>Jk{M6l+T9{*ilAU)(%)PODuz zF-;49doOkR%WqBW;`bk4MRmq)4Z72}i_?FK&I^*E`g6twi+w-o$n0H-#Zy+#TGOgi z!8HrvV@T^AiQ`2txLwYBt6p^YEZ7?dEJ^JA2VbAsyWf2}XbJBbCDQ>B5RXqG z>JvLzq>z(Wcxm+I_b(YPakG*3JaK)X10xj}Z#_rInkB2V#W580@c5Fjr!VMy+TNly z2KZWPpRLP0&v(QLgZEwAy7hRQ#3b$uS;F*WN0DXd=oO}Dj~oHK0$1|)EB08e_53TQEEkP_JjB>=M(FkQJ{2=?EW}+Bt^cgW?;L-*p2s;wo&U9 zlo#7Syo*{Z!%LIrbiT`BsdXm);)aKSjW;SIk@uhu|G7B6smE}zE_y9?lFqNY^Gjy* z+Oz5(cg;TiV&}*7RzmdNn=i)~Q{eeEb203g$_g6Zv?l9(?9n*Kcp4|JU(mC!PkWiH z2<5$9nDk@`A=;F@-60tQxG@hb{^S+ma?ak+qcf|mLhtR8rj~Yg2n72-vD6GO!qBlt z!<~Aay^m3!u!d4$FMKRht5jCTV{7!r<-Ru3Gy%6;24cgW!2@sf2xi`E}foJ7y0=DP;9blpBjoztqxijO(=Y@7fCrW@bEW_rI^^FsfB`D zAI5%nqm_0SJWpA^$wpdw&hQNse7dH7TRgYzH^*r)ky`iZBXP1nIaoWL9E#$H;Vf&W zY-4QEbH!~7-J=PJa}A|io@#50q<{*KnNG8yS77eFiq{Oo_!cU~nVp0Wdf-IMTTY`BQ`m3I? zm@9%`3sWMS?!Qbx0=XwaToLNWB!*3H{ozZ^mKn0+woBK2nV`RVqqBcdRAFL_Tfb2f zsYlYc#im75v3;R3JVHN{PmJ51=o%a8Fw=B@ zrV-WBcauJEOs-Kdre$=dzVUtb=gC3IUE|Jpetm8%ql&_e7)d$QBeFD!lPTvBgPmm2 z;GYoX$DJA_=EKD5!9qg|XWqH#)m%}Pz}?<4kqqBts!tJW3THob-C*~(Q%t;d!8`SlA0ZE=gl@4lm&Wn^EnPDy@|mO)F_ zESw_63pOXyyVB_I#xM5yYiskL_m9_oPL7owXV-mDpWh^Sz|=aKL}dCmD=H@;CbSfX zd6SF}q+u}mO8A^U7q@APjKN`-+UieqzINL>YvRDg8h&wM(-XeXBR^Nr@=WfQ#AI|Sf&#HA=9djWh2MX; zdGj8%=&(6tD77{?upv;toii95ER!{m;3l2n;Q zV(})TxBE_Wy@>Gy6Fv)hdM5uuzjd_Qf?Kj#3WMA*c;1an><`^*8mJY8eG%S+eWIb` zIOKxr#+kPhCYG(p(9l}ME$W>FDk-x;$9Q$wMeZMpxiztRW?ik$wR^8ljn-0_NwK{b zzNEB_#l1h~tV6j$QZh^y>jggJE2%0EaW2n`R`oEQKUQE;}FhMEbvS5_O~^{8g53g+lAH=yX@ z!qlUkWXnT`U!*z{B7#kU%FRxgwm6YVS|F8kooY-|U+KDM=V97LeZjTR77~RyxlKSZ z0yzhGtZ;g3?~q<(gZRwS!>8=F$*QK3xyOqNox3Oc>RzOFcK4;71ql|P!`b<%-Oi=q z&ChkzBPn0%&D8x~c*C-Aj06ty8Bf-&YVWj_&dBz`E$zui3QTSO^C@lfcF3oHe%nsei{g`G2ZzsLNU@BtfR_b& zgOpx^8>sQzIr+|V1CmpVGci)Q)kFmck(5SBU~Wc6VTgk<-$)t zl#R!|w`WQATV;7naA&_}QSpS5cw0Y2q$v?B6HCs#ZnlSYqLxK8HH^{H%A(;H!*0&6 znLZ+*YxM=nc;sj5rZ$N%HYI3TcD)Aj4x}HJ9&gAJtkd7|GJI{M(FzDuZ~GW7&CPt3 zbU#~SY3Oi;euZ^b#^(&FZCNc;K3SauYjp%M4su@_>-eL3lDDtTT|ax5*tC+9ND51L zZ&f{A`z#@tXsn}-Us2ue&b1YplQTS6RF7j4eY?7z;Vd=5CCm-bRyNPlY(G5}gk^Xu zDj6h;>+Id7$XeCaWN$M&zjE!@J@`eEd`}1U?-xZD*3~#d>*6KQL1daZSI@`eQaY75o{hUOGmK ztrUDWoXPduA*S@;YCrx7=tSlN_}ge|yR~$F`gmVY);}@&edAKX`D}VS`iSCuEaE z=iA=60Ga^27ivPlg?KTSJrnM{;>;{yKIol$kJq%VKelZng^XV;aT?5XbeJ{KyRdZ; z0q3Hyv$CYU%NZY?ZEwB2CFMGXU(9REFd;-!hh6GE{EvF)7EK7|`a4SF^zmjsWA=Vx z(SuJPAUD#I^i-05X08V=T&Uuu+A*rC54C<`jN2^V3r^K|7lLrCM0I%X?zm6| z??M0FA_rMo{kArEe-b#|Y3akYZ6p=^aWs@BjmqO~ZRFW( zMtzZisEhds0Zcx*#Qch{%{(v$mA}H@O0$&)i?SF6Q!)R<=>OT4T$qYHOfhzD%qy4* zB7T1gf4f2pmLuU7_zG>_j1qwyfY?Y{DiuR8z8X+e*h+WZ!!fgP zwvn!qXc6aEtR*8m@iq{)c75p4^o3fd%%Z6vj~~^{{Wi$h`}q;WsO|3Xwv?H-y>t%! zYG3Rr+Jsu+F}$`8j2Pi~X0DCXZu%Lrl(=02E@#63LP^sTO`{ddTg2U_w_UxIe`~|D zRw_h9w4fW0w<8VRGXHVS#in2DF~DmJ^K&lqOZpOH-C=F%)Tom41VJ!RS7o3#>biYF zyV7J385sp$2ZoS;PQQ@BEsg!>a~cp7Nt(!n1jqO1)XvY(zg|(Tl-J=WVH6+@Y>sH# z_;nBLXB)y!799CJM*_SSGC$3CUqPsY7w+I`r90Q3qHWQZmtR_#;)4a^9xu!Uu?+96 zDoknYo(L6kOLp(w-wzz<-oV9m9Y4|?&{GZ9-aNb`b{+r_~_q|3npYS_{(}w4h$q*W{hi5?dhqM775OEx-}8} z7z92O?%73flwujYVgqC!Rq#2@;5_!Md2`#Qdw8^D+!sC?=Rsp9~dHk_{JbL<0=CfxDgmg^x3ef2(DRR z!|{Bu$KMX_uC?fHLYcc0jIpazf3u;ndvB`FF zBINuxXWn*TOECG?Q<74pbU&!4HdN@Cigw+W=OWq&D}?-=ae%HyNm{aC$MLP3BZ#-2Qm3w((15rf z|3l(_0e9b;3xd}wf(Qz!)%Tps-139OwyIXSM?#i5ORG)gSCrMdQn&jmudKoV6?$mWlHkJ*5 zj=!o3?YSZp5Emi=aJsFzaFm77UDWt!Yn-+cJqwAI?QdxQHZ|vmPcQ(m%d>E3{9}H6G*<+8fOV#16T)&IvD0Kh+7OqEh9!k zkp1(g{{U8b_USy+WHDV8LmX4G;Y7^u5Ed2AL=wY#Zqpyl`&{4(LW}K{p5CpH4EYNB znUaJjnxJOHpOtA}Yj;;h*9$di5JFyKL?;BK5P)izEaQLd69zd*|LB#9sy7Tx@Npv@ zNcrafXT+UD!OM8yEfO0{UKZo~kOPUhFMA$l`6ndb!k-@C6wHxju`bC(af&H2A#iph zFT+KIWJEDZgrL{URmE0RJ|mk&OXmG{eFvOb_pf&4sJ7rACPG|bPOx>FtwVWm$9l(M zP_rM`96WY93Zkb^!@qxAB-EscAFg7Q5p59B{^6#3oRhhl$lFkS(lefC&tqGC96-wV z9}@Qv-5jE-UZgc@PH<=3+V^7J**-@^eIJr37p@@$$X`Hw#AtzV@|%6l|C86$)y^nN zz&oyLAlZ(+?EVD|3KiYr3$Yfrvjo?rqO#y>MPN=Q#^tjqrC=&w1s5)L2Gz1Zd-L}F zWjM=K`#!w+tS}RYUhzetK~z;?QYVbC~cpeum}UEM%*`|n!3sZ`~ukKJy*t=+#-}ZhzKsQ#6SS>*auXY zrd|wbcPYc_!7xTPczbm7z^8bLi09~4gn@yFI||-3!prY41``|prSdVDsifTvih+Y< za%mVNCg;AN|DMAvlOS2}|EzG!f#u81OY>q619y~G*Z1~$OuQ9{3)4}Wlv01YIU-S4~oRUU&sQ$K}7k~s1FZu(DU&# z9173Z(uggaR!VG#@{g+J_w1SBBXUM%w(o{zsly|Uoe)XH@n!6-opz@-*r$L3!*M`C z9^BDQx&8_4+mdiOLE%6NkOxJu*T-&5^O)M_2saIE#YHIWv?upUr8KIth&5(u)-RvQ zr@uCQDLoxv=L6AF3EtG-&My;->KQ!}45tOLTT|zN5H$W-2!3zUp3oiM2D%&{=tXss zW1^@|lA`v#wD8G7Y1IWKASG~v+i7fX&lK~zJ2`O?RJ~ve6Y*(MV2R*(@!Yc;M%GLd z%)KB|%8}}*axAR4MzB~3v1z|85g17k`;L*ekj3FyXWpUZRAjKDo(?W za%AizWhv-UhDz9-Z!sW#@#AN0aW*NJX(0rq8@WpYa#DneK`B&@QH}n7AypAAY!ejV zy3gs{TL!v0#zuD=dV=IHN%ab_g*&49=DtKN9mtG;hrPC*PaAZH{cSTh{!J~RaYTPu z4>3}jff%XyJtNt+XW~U&6ueP_z#qkK=4T9?MMP?=_xlWkgK7MAljPzE85xi2a!Z$2 z-=ElOwOt^52M(x&(V2K#2cnoWS(u8JVS6k4gQe;07%3%&qGkdrwVBX~ z9*>8FHo>&srt5c_G*lNH<)MOVi8+ur zu}$C)1@x!3WLb{{W)M5EWKEanhrT|MjCG%%XO%V{8_-`UVNL$kAXb|F ze5DMNHF-C-|Hm<9c-|G1fk4kQESS1bZ9TL)dY5B5O?;o(;O&MDv7}nf4P81LMH8wh znbiQ=s%q_g$*woY9LU`H)M7;SW9y5J%O{Omj@j%N-Tb9^Yy~{{#ZV0*stW`Bw%oYhvh#lk7 z<3c49mH~yxhQm>SbprOm({P(47`>H~NQaG_Jt8oizHW#e2i~CM;@5SgrDfpsXf4ev z$p*>R&mlv3Yx3hwnHG&q37we-q31ClXM;1WBhRHJ)GJ~RK!~FLqGz5h`H=PeQeidY zD`8u7|0ZmPvww5!|K~dPiq2QxQa=x1C{bAaEnl@#(wuDXbqTb@jo6P@LKD+4guwWE zwFH*QTZG%;sbSRHtik~g6M*Fiq%7DaN7eMB>!FmYl~At$s|?X9YyK&;HdBHcqxuBz zxX6+!P<714VhX12A?%1A*dHqeW*NJFNXGqxIcK4u`tkhwTvf~L(nM=WSOvpUnQ}_u z)FVYhX;_~a&$=UP*IRxP5^)w82D9Pk!smNSPzAT1-@YVcs)}jbn6k599nztZ#+wdn zVc2=w=Wo%Kur7>;;?SlBNr0ei0D?125jA_qd%}~P$-GvpIk1sibnE|04J33*M6v)i z+vh)5$X&^`w7*Hw;+nOL| ztb+o)kl+sz#GP_9gg6?3ZQT>Yk|Cbxdd>AT4H>e5(o>_RjQjLfgalH+vl1D)Qyux}hM_Jv`W;sR}RZvT4$k0dTBHf8EVgz!@dX!xGao$p7i4M&=hzMK&{l z7^v)IXsp7Bxcw_?*pShilU;fMhHW0-EKLsMj?xrF8l=|RL=QcOb z)o^cVo!c~80?HK!N}(+aF6bcI)~T@jcN1r%P~ljZnY9;TmBOThS_$#vZlWN-i@>8pH;!$kgqdSG*QNT~tn`187z=ci)*2$ii=>pb;3**>f+Y^^ zqTsB_g_|O<|LLZ}NWg223`7j*mWrR8#NK~N%`*OIaJ{Arl9Sy{FOZW6{I;H@1b8sF zY3ag*M#k&gWcbr2d5r?a1iEX1;(m2ijLf7crp@XAXyR71ElYHyKvO{q32?v5YbGE_ zR19qElE`0Z7AH;_v?_xgKOU!7yNZx-k5V|io4R&7{GxrliAA)5vpBqH4;Ky%+1=Kf zf5C7wpyYUBBDfNb)cwYCCeyj9Dnj5Q03 zhu`89+5yqFcRX&^9C!#*EDHW~R`G2!9G3CsvCo4bb9EUfPTVbD$5ZT+_R?S=?CzY| zYVB~x-zAG31u$?PZ$A|vCgoDfaGi^#lmsrR*caJf*JQu>Fw2ZUIYMrdQNm6_or~rh zA+^n?gQSYp&*J*0{6t?9$<59$FE>LK zRw=m!E;>rOv9d3K_vuB>c%{|xO<&wM6}`g+h#!Q)U2P_#O8If^wegcZwvRHp$m4Obif%upqJ#ulC<@Q8@8l8L|r{l zTR%*9kmrS20g7bWLJtn2KK>mNGkq~pO5(PKrbJ2Xe83=3c*VQCZ->+llDRr&OIAU4 z)c^hcpbOVSDQAb^(*76nW79Un;Yu|$c7_{zm{1zEg~JH$)Ic%{StI&;B+%=tsil)$ zCF3#MNb)DZ3>tX>Eagu-2TKdd5D>c30|p59-`X{Z@TR?=8ft~xX6ErWrSDJRAjZ&Di4DoN~xT`;{3!}O#kvR`*g6L zhRIqw{$3(dpdai`5=YfIP+Mn`?$8mk>GKG%Ijva}w@ZPpYm-bdk{Gj3#zR>9r=5e4 z74(JLZhi*X{BP|VTxm76y#x%kM2EX6S#LCxoIZYF7BeC#KCRPl(OG_RqV=i1`vy?7 z$b0}z7$0ic=gJ_X@K>U)O?}%}r*V_33F(9SUUdHWtjd3!jO{%e_@iMzWtVR1q&qEW zF|Y$rZHqg?;C{dJKb?$!ch?+&GKn39JaLf{UOcH^RW6CKT$Y!XArp4T>|DSSW`xWj zCr?=mlFOCRuc{t(U&(wh0ja&e5OpI7u2hlDiaDj344mmxX~+#S-S^Cx1f?Zs6W&|{ zdjIJxq3rqhI|r}nUA@XG(j&Zkg6hcG}1}Nfo~U2GpEZEbtfXVRDeklggu#$@QNcIX+rtqJ z|BWLpO&O<32paIdGs)&6Dm*GUC6_Bsvbe5b2cZGYSG{? zm-ene3<}dWCl1_zI=$7&_%xOVD)s;_YB&wj(<8ubT)&{~wT=L?myd0o0NX{9+# z7Lns)N+p;bNb59Q3$F9*21OaYh!6G+r2Rl#D@F+yBLNu7K%^Wo`c(hj zP~z7?yoYj%4Zr8i#X=Au{xb%^ciIAGsmK-s2S4ZRBA;ji9|_}AGf;qll0kBSS?%wc zbI)1R_vwqqtT?yn&`S)eB3rarWn)n8K`jeryZoiKEeKNp7@#@#+cq}j8l8E!z^Oq4 z(so5vG&w%6XNqt-mx-G_MLEkdG-m|JrBzj3!iVVfK~4{WOvIo;4w$^*&H3~HQi3$+ z@H?$gbYUy)t=i?LP+JH%B_6>S;2^*<+SC)l>MWcQ{sVNoDYUyUybi@RDDAI;zZar7 zwR&A@%fDzjHK?Lgtd>5V&ufiKk^Cxjz0OXPhX6E#($)97Xj}AB#9>js`RDmRNw?KO z-L50k=t#wRD95xmKZiWn36FaF23b^}+U531uJi@LmpA|8g43W)uUVE!0I^(rf5nB8 zUrL;PdBsPiRUHDgdpkNUhmPjHr!OkOck3B6k{+dmK!e~pB~iq>K$(a6J%7?Z@<+Rc zW9!X8|4vOkx_cTivv7(4l`}~l&)KhYOb9uv^g68@>g}~rX)=%pkN25mRf243VGP84 zeC-$jL2~pm85Jpjb=;J^ZfH@TE#L$Ko)^zReHYIT9k+jDVuK%niUP|dT(%8eE&UC| z^zq8&9h<`U)Lzv8?mQH(`GD`{Fn%zTHV7HI-$g==E$ktXb_wt;!@ay<`>MKU3#O!S50k{I{#$#|)uWGML=xO^2C;=?9YyQ5UTEzRA zP;n1*I=;gDQ4LfuHiwJh#q`Y)YGCG|G?F-wL5>xkf_;oJfGWp5@J~qpLsp?sh3nR% z&V}-r5LfLC7~v2A1%^37wW8)gnmeGbdMJs8VGOSI4zaYVcL#TWPtoPdwPR)JJ8wtD zGghAJtVL-<8Wg`5m#$iH4mvY~jV_vgc_KJP3}b}Z1tE-ARxNgD5<{wP+2?#_DiRX+ z(tt5-ki2~Nb@>~pf>xLJ+ot@4dRKS3{n{UR-TihsK-AKqwW%oQ&lbys3~)NAKD#`J z2wl0w@(zA#O$Jb+Be)##YBb7j(`CU~ErLJs z4_Sd#7z?yHUi}s8ys8mE_@7-e!Gu7S&c!zg;UJ}ya9aQ!hz&r+IfdlVv>DLVa7T_- z$Gz4N--vMl{IM;ddI6Yc2nt&^0RXZrq4BQnCJ*mC0Fl`^L{&bzQ{)UOf=Xpt{xxXc zfkFtc9^k$Z=@}_D`E9so?YC9lKu6CN@G)da^kzu86i*#n;{MoY&z1bu@0=PDn3%cP zfpTsea<~$w{{cH;*{qD z*zHWFe8U1r8ntzA*S+Ue({;xhg1kWyDRfD31~VGjOaxoAP8hc*-G{h+D}sLQ{qZ?m zGg7o5HawSrYIJYT%zpW zlngCGmgYp^l*MLzV7dIy!XSYU(#&syN zE6xjVLZxNPuZ=UUYt0hVD@}6SjO9c37`ww=0lbo97TZ}4k};}=V4i+lGbalw@02Cb zJ1j&16%>Ptg9w)KY1;}>QO4rzz)uMM*IyTyPWOITitvkb}^3tW#8oV`;q4Ez;_Twt3M2RJce=VbDhp1n*@?#`(k6=s(z;xr z&^@(naf@?pdA~mx;YlxkT_qX3YeVK`CFuOp8y%RNPG4hMx@_6lSRt=@KypOT7fal{nae-GkSGuv?u>c&7#pVGlHj2H!MS# z&Yn{j^Eg@TeH);QYZ6Y`uKvsKpi~CQ9pfNB-P^y9|578Y$1%Gc_s{jmoqShwQ@9J` zFCzeJh?0dH$ksburx9(T!m8Zbr9X|=vF=7~d;xo{R?pdwDt_|<)^)p-xVi*Ht%n!G z#cZKqp+(D=ivO|>%+AfheqUhaX@dZDmS zXn=qMW&lM;u#lSna*2?yMMIIoJ>_<95HM^aaP#*J z^Kohu;8(niIw)j*@TUzzTFXr4{)72#f;PIx^jdfG-?jnakUzXPDx}rq`tj^G;QXSm zELAxCPb~eLRe!UJxBq{KR;lX>6kf8^8yl%%A}N8vIU!I4qy@@`xj&Rz5sf<1Y#dt4 z*Ew9>YQfLsJ&*MJah5i_!ht;=>W6Ktmw>r3J{xAW)D8{<3QxFLNASbd>*~T_GSt z{fEtbKNKJ;s*pVCb9Aj>Qs?S2V5|BhV8Xc@J++$P0nE5!C9 zSUy{|w^abU&v-(2>c@sa)dPUdINqm$nl7_*Lq*VjDW=pw zJ$=V|306fWU$nSzPBdJ)rM5xikaJO(LKxNGYoxnzP+8~h2SSaP?;+!MT+?=oP#wNMHJfRmSn+toPvoa}r% zp$f^@rlJl}PMrO=JuCOW9bw1IaPl+TX5Y6iksyxy5#wLyLh>(O)UxH(3{3|BU4DX= z!@Lh3EY8pIDIf#~@GA*YW?Pl1@zxC_5yk!T!XF56TxK1IJ-H50UVsq~5E;+bpUREv zX_s$zU!OO7*Z$aaPtSTk6|88fOqew_=JGykjzzGi#!sUCNJj3Ijyr=fLw@X8U?8E?JUuDmlLCiQk6Xtt>~p zxbRkNw8G(m(V0wXXdZ^@anUJp+^%(blwF_dJ}qpyBWZ|qDP7koQEjlOunE$do1T}w zw@#ydaMcrP9l_!`90sAuh7S;7Dm<(rn_>!zN>1xMbSIdg3!4@e)Bz3H=ML66&L z?$wM1VSax1?*|tsW2CmF>|UfzP}5Q2{Id~ADNrM}Zw*gwA8L)W$p}oP@Ztyky=;^x ztfc`02wgUFO@0<%7ke>sv#?*6`B>IuF)`yRY6r_(I>-1BFmiw~woM=iw}w)6!=V+f zRei)uj25cN4uEiaLk(gA4|0k3t4R(0u@LQz)sE@b8xq!G&5FsnkvphP#9% z*B0QKm22ypNt}EdmEha*%5@EjZu}h4h1oSlEMz`xR7D2I=89Spg=kzV%;H)K6~dWb z6HtEU7k=RRP03Hd65UguI_rFInFt7~ZQ~9mcKh426;%l2oXIdcbCki3g$fK!2-vS! zXbI-e>XfbM5-`9KQmHKZV1n9L`@?SZsV{=VFT~Mj^1wWHW(inS!eU}NqrO2>HT153 z3YZ$Q`xraXgTJ=M;&?@VDYCA3C3afp98dHME_6#e$p?Z}iBE*AwTmdE#*$l)ztr(x z;(|{3Vm5SKSeIoos>9s6`%2+@X+xx+F-U|gAoqB~^JaT-k*xa#!}8-rh4`3zi`H`e z7OCU2iO}Q)d766A&VngY<=enymf_CAyuIYKD%<@DX#gxh&yPk;8jI7uy}%vZlVJWF zVHp&f)K;U0orYZFE}uNrfSgh^aE!=7tvkSB?=iO58-fm>d({sQM;#s$hjmOSDpJ zqZ&w%@a1}beTnV*OqtL|0( zsb;gBX(|`P+V7N(3+S!t13wF=tR+++#6NW7vc@lj?TJW5K%K;>bZfHQr2bp4n;cs~_+Xv=|9slbV2SC!bpEIJ6*548zI z_8wOTqRq}75$Jq5)+3xHM>N%iPA1Shs6gX`98;=M5~@u7h(L7AXvyZ@>|Ndq1)iU1 zb)iCzgn9iHM|1_^ynvAAM#GmCYorUU~dMN~@rq@&1iHEgB=>{wsRfu|%#6a`z{=pU; zsk)jk*B5CctLo}% zV+3(O=dIy9EE;~5g zx;;qP>Xj(8l|} ziBu?#MowgAmsS|aSt~xDD?&(=0T|&|a47yt-shqcC0`yuDuT%-pap%_W^b((ZXRg; zcxQAzRDGk-gtey-P~Dc_SqahlNhr~T9j7#)9#mO7KRFwy!7jF)UpjSJkU?H`*r!{l zHHA}g7eq{Ul!yJ=Z}c&d`T2^)4>B?w>_vNktBOlNbI~M52+;&b1lV{;88ed*)+O%7 z(w9+lb`_+&KH?VQcdoE+NsujEr5K8C%4DZY6FmR=5fiTs4rRQcGjwX=TOQltW^3pbz`MN;Sv>-2wGQdtlj0B z6@VjZWPnEU2sHo!8XL>I|~D(Kl(?=nv}?}w}H zUwDtE-(<|gVi(wKmvx^W62U;zOKm-ZGm1bI!3?+Y3mPI7A!HZxVn@`*ro5NE(fwKL z6cW>;b1&VETk6zreI3+`VoK%$CMz=0FtvIYRPg? z!y+_@K(C%eqvrAcgLpKDuubq$IqY6C{?vN?T~?Eqe$S+m6iC?jC*HYk<>AZpEG%UFyZdoL+p@uIN?@5-+ zG2?h1 zQ0LV73#Xg|+`4|hw%%d)j}NZ}j+UHALs-f%2DAvOsJ*|lcq2@G6n#)Pb=;1BtlqNM z3MPM?e2kZ=@QQ<)_F~NShJP@N>TrfbG545VS1;YktW?EN z)#?Xr9pFa#K1zwdS5+|FJ`7;W4@#DxO0WA^nKsb0j4yqWd`|x7>ne-VGwccCc4)){ zKnaO5g((Q11rX9Kc=NSu@wN@8+Ty??ST(aCIEAYy#Y9vd=3^|+$aV#gm5506AW-~h zq#TB34v#v?viD%eOyQ27c3LCgDwsM8snvi(ocze3=qGsv zZI$~45DFXZPn-W|SVfUdshRWXzkL=uEyK|{gLNor#tP25qk>k8{nf25DG*k*u!DRZYOD&u(TlOX%(Cic5J-uts?XT zU@ALvr<2)cdh!G$a{%~b$!DgPHjZpmd`G0|5IZqk=p?KLpL6ih@6(aivN#(eb+$Vr zo?`g5RgWrB{FS*;ehFIpY=#JJ1D+4TL%?+z@*Fezj7LMd>Kk{96%S`pO3hcEEd&e( zj7@^9SLAO8&~?<~35l~_|7n(LXqz*PA2Azpc1@M_dj=pIJ(w#*Fh)I7U19t#Ee#?$-dVyddD@V?b&Ejr(C1Dqr1 zUbv797>o5BtbKM7ui9%M=o=OQCUP6=l*B6a-7cgnK;lQh_JWC`dmJ9gpH(eX)2#f@G17fUSq7g~+Qa?74FN^wfpm_a`q<+Kx4Td6PR^ zC*^X}uP<)BftE*w%!@S0b$Lt^d`e+s3(@I|@#Hh_yjsb$<3va9WG;X~?a8 z%%hg{*=nEMXLGf`(X#7pSzgUfNP9&-KTNgom3R#RA1_cWh2R;L9IYy^xMs1{%;LcH zE;OzOWXm&ny#kOAf(}W$t|B|3M#|Utz*H?^aSFi8Tc^Hgeh3T-lCKV18=3bSA~{Gm z=lzQVMN5oT@@kpSbb4QxsXP4Dikt&TX|K5`V^52W2g-WgI`q3UyqUa(37PMckk@cR zE+NAE@v{#DyF)*j2o@gR*Rd^ie2M9Wnlz#C*5-|WdPJ<@e{#DX=lB-_1d;q)JF&!$ z?@?X1Z!x96Vvl#^R5hUJJ1|Bc*y)EEFkvGNMNjrA>}NYdbp;F8?HhtOoD75hzA#c_ zv|szFx+IemQg6rSar>weA)5&hQQd$svVq?=&?ilR z={v82$~^$&9ON?Qty3(RiaU(VK4qTj$0Z8>yAR5K6m_}Vr=d zf7ZVK(WAxiWp?f}_m0SyX;QT6dT9JX?~l9}4ZtUztO!iw_(-`dscqvA;h*NDwuf3r z91htdXJ(dGm;Q=(h#)PU6C(L+Cm?<1zs&2nQkqY*!Q&(DCo3f1&DFUc%Wygc;}qhd z`F#NJp9s=p@R7|KXF?+dN-vQ(D=yrIGeSJF7cJsk|>qDiy}31$7_U zS=m~f{%VW`6PHVFLM91rVE{Ki66c80({Xh$@^jD0yzL>-<5I($?`68rXUf@uo&~VN z(p7hLuFu|uZ*z4QgONgx>6Jk%KY5(HAma0^SAz5*(D5Longls|?a&s1s8n7w%55B5 z(4wsbrboW=&M})miAlP@PK)r)^qlI$j3@h~OgF0-#`;--Kpw@P8wpDSUHTJs3mo}t zch>gcGS*%5a9z3tdfGOI$?E1M^r+}j7Z(A0$hgHQ82-ACg@UCopkFufZn(f%QUBrY z&%ul*^uQsba&wWZw@Gw`)VdmT;J9`^io@D1vtP%$A5%bm)V;0m{Pie zB|gwD^2f3rO}nw2yxr#tzCbKDulN0lLxldg4T~;B1ptc#+gd*8zlKmjPvOpO6{;2&}>pq@aoIo<3DnSKpGiqr*qn4kYlR@3s+asPn z0<~D;@KhRd+cDVXGvWd3ViEGujF%eTJjVXAS6YP6y(6X{cG14x<#k!d8|bG0?%ka3 zr_U$m$sF&AAJlv>thsZiLY&fr26d8}n(+h0U;8L^*;Pwp?^YmuU z5S@SFA7|(!_^6{t!73QJx6W3Tfqu-+nuY!TS^Jb8^x{Y#?Eg^X5?GkA4)j5=pJW>H zW`$YT{aK-V_BPm;hkdG?%t}wv0syMh@lwy|xdTK_exYQTO;(0tz;8z3yBW9y2b!M`hxz#-guD=rj>ciZuphS%Fsx#h*^XkuKv8(U?|OJW63A0kCjB)r3)f245gMR$0e+YR8C z&jvIUVuGs^^JfM_B=i3zhoz_G|K{-D9R6E}|F#nUjUE1fY$X=I?Ne8RPNA7@+r_5j z;KBp(>Catm?R!^E(}cNoa<~^SN~~{9O5*fy+DyK`9csdKrULRr`X`1#kRO0-(08M7 ziDEetEmQy%LH=gQt0lz_hJ0BqfHG-?GZf}v6ks9*oTQqt#fkAP3?p-Q3e{0$Y`_c) zAJjtkNVtRF(bMiP&D%bJE|wPOc1b7(r$NWMG=O9-XRLGF@eU>lp%P|H6~dLEZxne^;PYWzmllDu$O)y9`qdX3q$5RZk5* z1ft7mTU_rRkLg`{5n`357QWfdvYyauFmnY(gJ|m9wx9v^5J=IUy0!_qh7v+qC{8|H z(l@cv6(S47g~!h(xOap~VPqo|AB@I~;G$_%hy!pVgrKLjj&>tf=b^ykT;@@BE}Dl! z*yt{Nw=On7xFNU$%y{oLOz*G5Rb%1X3NIjjnjJbVwF&^lvtW*y7EIKo2(!yP7dniY zLjxS+MpyVe(=o&8_mu)zw%N-@Z zWUd69z%XQS0C6NNqvu0b1r2bmK5k>(M^#d7d9K3l$&E{hPk#jsH3^_1_iH6};Y$U) zv>Y1#^ZZ^I==@tAZm~qFG9f<-58atfFpf&)@`o~*@hZ5kr`u=ihjM!GSsEc)pvnZ{ zKw!{h!PHCdK}rAcJ=F%SxET!zJ`6w~1bRUq4!9r7cubld8a@6mpAL7D8Bp@h%HvrD zfH7tm;I%KG3V}KLS!019pO*5U#r9@_)}8nfKDdJh{Wij6Zei#q)B#YX9|C1$%vLyv zSDHHZejEUlV2QcoDw{$;`CQh{W5YKrVj1u3{g|3H(3g0;YLl1A zmptsgRQ17?^B}(ms}n}~de((dksr3Y%?K5eyv8tntsZ7M?Y_VC&^kUBQbfaOJ~-AK z+D;G*=zJCwS%3(S>3<@EmWYQh93aSdW3AKPdl$Ye4r-9Z6yKx=70{t6S*Th+U*8%8 z^AP|h$cHaJL3Z^sN;ATN90strCCub(?2yFFcWfQBR(MIB>VBFfxTehOAQ+adt@pwlbfZ?*OV^wrOi zIt`!QfW|C1Wd5AWM|v^1(6(2ozZ8a@1YF3JM#na?Fc$^*jQ*;-QI%_XER&&zP?mAr+)|!Pzy%n! z{h6g^9u4CliuK4ca{ad)#WNe6^+q$a!~@143mm=&CYQrAeYc9CGRZ#da9ZaVclWgM zxPYRtc6ls>>Ut4$6x(YN&Y;*HO}&K#fB7944l@h;N#ZdHHD4)!0jRrE6i&sE9_tx0GgK&j&tHIq!8AX4pt8wLT`-I))hqhQ1yOh*%5#4 z9M=zHj3L&UbifX+3)2z3>Ui_7qO${11c_4e!iBxrX<}C{B2F~RRM5!MTWK zkTj53VG<0q3Zl=a5u0DN3gUWLv9ZKS#S=xF(&^UebKkyx1anNr6=75aXqR&GgfrE-nG8U?*$UOh6pw)%GjxZeq`~)mh5I~2CVjTX z)dz9FDRS}*r|zOm6)3q~zd)bBKDj#6NeJ^rA7@a(hZqahYdPiv43B(g(5gOlt)2l0 z+qeJjNkeuEyadk<{Q(3)U{`ID7y8gvmIj&(W6g`f4tI1y7wKx?Fbq_kD@=nicNS}u z?P{h9RvvyoAD#6Qp$?eoCJD?hz(AEhSe(4YZ|j1_Cj!+W?Kxm{jPO#pwSSyAp?*CV zG8tv}aE{<5&_Qmc_v(40&-Vbp`!A>nji8e8zYPDc_P#tG>imB^)5tJlNG2^A_mQh; zO(Zkpu3Tj+#iE3c{hBQ+p~j36Qc`p{&_xU}3-+#W3Uw>_D-ZSs#>v>gQ_cEjUHWF$oz(tv5B5`Sv~zRR*FMiXa(k5unv> z=)zJ0d#X$(3;b-5)Ni_J0xvS&?PW_f$MxI%{G51$o(fZJC3H8qs#5*IS!OuILQCye zJENnWkNH&{Ssd;8L34pzCH9ge#`Wovbl4Xa^y}0x+67U=>40%iNZf++n*eqjUnA2{ zKNh5H=#EO^o8etB2_Eg7G^g+mISC{&s60HAgvLCDc4-5jh&L7AXxWVBo4QzoZPX>^ zvnWlRCGdNwBhRR30&U_7H%_J@&lT!8FhTR@=dw5NqI0(RaFO?OE|g&)t(KPA>#JWe zQ548ISB@i^GKFTLH2L`K2A&>thBT#8hu?}Wy;Txj7pdM-%Y5^u7S%z9Kw1!+Xz+F4 zhA+stx?&~lCu)2%@=61`nOK%L=EICAu@ZNGeM|_~>~V|Fo*jEFq`@KsDXe&EDq3v# zV-}lO>zqL@Y8x3D!@sM%w^g1WzyES$WafK^n;(6mr%N8dZG)z_yj^WuNN8)2shN2J zE8E|q>c(af%ws+@zF4aD)azV&ozl&Z^{N@OHVXO?<2I*$yChPU=tYjH9xz-YbCmVH zsWUnp;i_Qtrz?~15Nn^AHTh1W#0h%fWTU7V2i2nH4a-CbTvtFg2I#c+Gf zP^<||bps)u18F@TCTZv+#LM&>+MVs?3CrB|5TOik#RZ> z2Qf|jEztB$Q>w!7+hq#?JM#0-QvFzSoGE_X)Ri4j=$+&HPm+bpD+8Z>u#>U4cTO{WP^A!P4Ea z^kmc$6inHz3l2;F1x)%EF#rF+X5tuf+Wi+-Fi&={g){2xl!KL|@u>jr=UxC4IfFNP zvaYbf9P2GSWg9auR5UNac0x2fKe=p{N{LdRHBh z$NS8Dh_~vOH-hU5a9JIu>CMgjgNPO#BJH%e7{&&`YtpMxV zVfr4D8&WON8gLnwG{H(8uolqpc8Li=5xV{Xy|`gJR9mVM0ud9^%v*Ge4)!OAv!@ zOoG8PkYn03ALs$=l;ZPg-`akIo*TJ!(8)Sk%SF-QiU`S#Vy1;tsh6Hdcz)10^g1r* z{oQnj^XJ=(8Nb@Bz$k3hBdKnqZN0u|x=Sz4H+tkn=6gT4)C!^Pxg8_s-*ttB8*{x` z%dm3EhCzB76CT2>#Z>o`6l~-;LwR8slm4}i4+*<;_r?yI`9$CA$1y4Lw9R_gX2*3YWAx>4f{llxu4&&vk}w~>pnHaJyP z*e^rt&mOZ~&bqLSiJRy{jF$Fe1pC&NQg9Y+c|~zS{Y_PuBsA37SCz@~M&O?(cZl(rV>?e3M~FgJlM@Nh6603Ds5- z0={BxWM;?IT7$tiQLl%6ko_5hO~@UksI1-N`New$jfxW_I^Ao9wKNLaRa|5wR+G;` zibHsKcCmo~qrrDhtyRv*wnXw5u%ih#w{`m)1|nYUWroN!@%17t0`mtb2KadAKNPfL&He#}1WKr=C5LH3XwGNj*%U2Tne8D* zyJk2`31igyJHB+?Q!W9-*VEzeKAvk2grB(B zF};CfndyrUKFgD0#-bVl1ufWt-wwy`n#ppQ?@QvkN)vHR#|)+0eAx=2D`L*P&Kr%FRYp zd5jdQsR#gC%Q0So6A1&{owVRCh0IYjAz)5e80^bdi5w5zL#db2xFRW+Pn&di04}Wh zA|U^%fY5~QY18+gEz&BY#o#VgF}64knJaWSQbzCJHhmGRS>((sdpSQXMZVVLO;HGU zq559pS0vN5tPY1>#`oI~8(}Q9DQvG^4=a6PMp?@?yyj5FHqJ}7ULN~EG7M_js1x_E-lw&Q;uI)_XX6+(W7S34r z?K_PN!6u~qQq2$ie)V8Mo)^jP>@Ia8Kd0z}7VibX-`3 zbywZlObyNVXdDSI?)1iO2CU#|#GwWnyo@uZIGCcS9U`tW9QJ#L;8MW2gN0!8L~ zkLt0r_jMoc%#*SC_qSGOj3_nb^$k4{fV`u`UaE(vkvSorz_k;2+VG}kc zJ8-lya!k3^tV5}iZ^vUJOX8keKtttQ__9t;(!Q7v4s78tc9&mA+I zNTd(6ZoaHSze95w=%B)xsaP*t=XBwQ9=qPxN=};`C?o)vVW|60$6Ff|LLADmgw|>R z&w?o@8)>u9(jJJ--!gJT;e#`rtu_*rTeyN0^~4!QFrIG`IsPA#&hLBC{}A3l$U&mx z8yFcIQiE150wP-8Z}n0j;0PK|I1H*x6CDtd69>d0DY-Ed#mq@6RET`>(swvmg>7nx zB(DbiE(KCsnOS(hJhH5Y_>&)P9vP(v?=!b8N1By+zFwkL-WKjyRg79!vq{E14bq3& z>{e}s2m(Q-pa}no%vxULs8j76Bv*%>vft>r_P=x67y~t+wG~!!ZYxWY!hdZmiy9xn z<+y0_6#A?AU$?En0Lx?IgAypF`@29T+Vxhc=Y6C$GzR~LGe2%x@lVJ5b%kA72(dzV z(1Ne6z>^O1=fB@ws)2$1NtKZ&PgFahXM3vk7tv_tK>JtIB8hlP{7=P6y5Hk)mGlr` zr)4P157O$T0*K!e9^)1_i;j)|58(}rvL{$zFHw77hDB;R5D{A2@{h$(K)H0y;5UXG zoezjGMv0S6m)Y={)%Y*^hWFb(B1L-&4>opcEAUZLGqWGQnHi)ZwoaA6FQS<(~vjX`Vw%9&Zmr2px7Csm$1lz{`3%hXMr=gONO`>lK3 zLIpzfiisyt`><;jQC?g2SC!H=v-f7m1soY`Ri5cW3yz?L&hEm$7c*~e9t8g)w<}_a4-w0V4ksZEN~@SjJ0vp?4BWam~g`s%MJJh3;3{`1cn=63hR(PgMcEGlcn<1wl(*{Vn1_KGwQ2z zqeF;pRzEQG^&UMuXuDao9C2G``4IhmMUxd6DQX4^=m52YWggp&r>4SR77x+T|AT`c zMU}+dF~nejW7jo<*qZe7_je60-ijI|-#uzJCHP<>+=c_Exi@c|NcjB}9>VK0TSVvf z^B%oa%zqJGxc<9y&vQHCq6rpKa+1L?RO$EOe)=~8rsQ}&}39(TQM@+a# zo#rcJY5`q&)d7!cmm`?IcJwWG`&V1^g_BZ(UR+m=M;-PG%cp*^jt9 z7iQhrxGf^d#)5R}X#TwQsO$hv!0&9R;?)He!hs+yvYO;WI=f)VZaWSKG)=|C8{@Cf z;Lp1Q4qYYA{tMPVqk&RNF$4vd# zHp#LqCAXsdOd#XH&jWzfzD98YSDhLp1ehBZA?nX*l+s4&3?=eD?}%Q%>gWs=Y^Y2T zsyk3tMuEax?K=v%fSgR){94H!yZLKN;$rCOi!2Q29XCfCs$zr|=D02;e`)YE{>o|a zErx*<8-K5jcQ^!iX{BDs#_pnBf-?+>@!dg$a)W9`a1Fi?=nH|qNYMYg1cB{jt-yN1 zR%{84ehoSKRWhnrKO9Il&OGno1Ld|rl^-T{MiSaCsU%|?_UK)CasL1jko z+5O1ygO!($@5hme>zIjKyawbfs#pQavKs0c zH5wk9t-W7?_Y4gpdSBgfVaE`TZC2%~Q?aRY=LKYv^B%6eYzg%_j$je<=H0RDcXBO} z3gI&gmqso2E&8PscEtB4PwllU7HULnB5C#HRcqLgCGEf)k){Ycv)wPd^9wGI_OKlnjckVeEH5xu^z7feUSJ=PN zBLQOc_z4ok)5+MepkBWN_KZp7WrBY*oOIbhjX?#=6!JX7VwD@@oZ1|>(gufZi!F}R zy);v`mr`mQ_Fr&%OQd%kJW+~L8fxTY{kNcIIN+$oKo3v|mnRjBcijl;`jJV06C+f# z>u_?DuFuuDoW)Y_Oi4dC(j$t%YQ>s?>0#Ps8098gZIEXz7aP)ZzNngi^ih@LTDlc~ z?mRi{#_$dsWjiFg2p&G30)}e>vOlQ9;Wl^J7T+1E6oOpnygGHJUCmIM4OL!P+X_D| z1_`xP@fO~WzsspnB@Zz#lDc`(ow-u!x9ay-pV-xVMm6pc(?b>YkIeNYLuCTlT;K$$0)Jd&$bxuES6F$c3HsqDt}(gwJ!%#2 zM0`|n9?hpX*1It6BIzjxUH=!zE6cPk?*Lt2wr6dqlZup=TxF4PHvB=+&8x9_#QMPmRk9R^@80aJrRyIuy?^0wm($TYauhN)V_l6{3O5d`Ltyi0d9G zHfyt2Gu30~Quza#-)vT2eBM&sV(k7tlie9g6DgMCL6!9VE*$&%c&MIVxJ)`Lk!I7R zy=ffON{H)jO4K#_rN!ran#vD74yYvf41{dt$EJ9)4F5QlhudVI`ZmYjtnY&ZhCZ+q z?vIkxx{}d=MrGB$M2Ot+H-&T-sr&Zh5-pcwFlG=i0Qj6Tyi4H-ty?8ZHE#zH|u8Q%SxB&t^;M zW0?(OL0xNpG4sP+3$rljr`GFJ7taN3&*OMleZ8phf5f?cs3=ZSH3(%AHBSg5LVEB4 z_-0u&`R%K(ww|0y=))e7RTP&G@{+u~4}1A{MZ*z_@8s=Kb$wkZm|ME{ zjO_wxaRH^x;qukBhGwB_Lwb@recAvI8linn(Sn}SUqg~>wiDU3_#SX!Bw^(`kGQ$;FkC}lq5+cXc8>~Y%!iGxs`;7aELoOU z_o6iqa$C7ajUhwyKgD;0$R(K_DwNLl4X|D*l78%I+JcgIGUv82T>!&QYJK>cGs(J= zHaHcXLfTwBRYpjg*9&EZ>|I_Wm4nJplTDX+%F4G1hR}#O#k1}#WK4=;>SS5E(NLw9N~ZuUQIIo{_PFQm~g=DJYjX^zsD|D;e>cXAjZDrXo!XU5%ni+5D2i-i=p zSbaS%(t#W|+@6l*JuDwc0DM!l{BYapK_rNlM`#0Lg{e2XGi9z$x~mWp;l@3cAJ`RU5BLUM!8RtFqjjEx*R_&)eMUr;b}6GrXUW-0BI|M{=G`Ik>Uvy<3Ixa>rS1F6%V(C2j6ykByYx zboo|pNRWq%&vS$1;6zS;x_}s0eMFHWiP>*AB{_qPqxmm zC#9fdA+C#u{cXE`Ghz(My@gdz+J0nj<3Z4C6~Zi6?TG>Q>ZszW+~jp~drtqnR42vM z@z<&~J{@s(urFRPctNLojg23o~n(Knapa$|lc322(=-%8i z_Uv1KGAsmpNOaqbFGG3ai1t~Dne?fBYgW6v%b#HkJs>?$bJZji&fvGxP159Gj~lx< z>TJw-m_{xdN%r9NuEaqtU^!Bp$X=Ui2D0IS25TNyES?f11Urm->IkB955QQXkt&p$ z-CFm|!>E1$Y zHW_p1MecKEr4X)Vl;kezB{gm>6U~hN?ZJl4P+-U6QlHd{gGr>*o|ffyz8FQn(`@V3 zajYRSZvEl4G}{-&fI{=IH>SCLJgztia_J*Yi0KiS6X~Ix_WXLE@XZYc=4XThgPEj3 z#!>|>&PNOOWm|8U`sLs%sJcJ2C!0b3P&nQ23 z7D#+LS};S>8AL;nPs}h~jHv&BBkrZj7k;yJZvv%q2?lwzGfoVh!IfZWKU5PvaQPhv zk#)NrpA34@JYPS;ns&l_KKqM9XI}b3&0&p{?Cl`kcfHm#O#X-{K&$L8Sl?N52 zGwpHf1l`B&dev|B4?QqcSC#(z6M#S`vfVVX7wPn$v&T+uo0!~rMm`z)nS59OL_Rd< zoM}(fQ($c*lizv>os1)=Z!}0bVLoe%r@w29xoobRW_Bu$YM{!6kTF-eVs>k9td>Lo zlv;t^&G!PkPecm_H34ssE@tSR;=8bUN^;GR3GhtnI&yWa=2T1(9u@Z5bV#fmimarE zpAu^aNE`f)5ksWVA=GxWu-lpPNyr;B{v_lOeZF?l4pY>CS#{=eXBdxjep0^@P1hz3 z)@HA;hU`m|BK^VzQ?74Mk~m>IqOE9^NJy)#QHgJ`-P8KweXmXGr*=3`Nr5A*rt7!n;IHZw%>!0_`xN6T zmHpKb!i$q5eokChMJCPz8*}BQx}Y)V>`L-+UVP1UgmF^YqSNH=Bk6A=?znFjsgqY0 z=r;8YKoO^K=G=T}rIC>k$Z2uCmtP$oRe=@_=v5xdDdTp>H6W(j>$w>goC3Qm^K-}i zL;x-a4m#P24y_)b&_88OZT+6K$m z$^0#&4)H%7p!e}{_SM=Rn>kVG8SD6Uj}MM4(@7bLUvdBshN(g+?dFD&)!Q#VGpK-4 zL}Y2fqUom!qta=fqU3Nl{r-S=Q?nCBf|vVNR^+i`XQ7f**8nj3Kbl>ehozVaC+APnm`lUuHJOd;P$)g zP5!9Wy-5GctlALQgJ0J?vR$AQfFkx#}2DG8j9KV zgUo70$T{xIn~p7#d?%M`8aa-(zN{Ll_)eL>6+HG$pd{wFGbwr1-NY8rWZcs4&eh1|LI{b5u=mV_c*U}joLQo zKR8MHyF=a*<{m2RH_?kg%U#dHI@VM{QD|drHl*}E5sG*9>#3EifJ&D}uFZyYnQ*N) zltm2ZfYUoiBfI#`RWBL)nWI|HawjO*4VYRr*o;-8(NBfi*iA6XlBN@AW-&vKQ(%dx zui^`aRJkgv#Q2u9tKW0Ldb&Vyq6pbOHAe9_sj99gwoF4*p-xg{tExc7_7A#wcE z?^oG+@&n80!)D@)8}alVm9>ifAQt>1*oW7|ED3RJ@Yy?=vzw$NJCGltUN`P;juWP! zI+U?mCu7K+>J1JZ5j!VD8<+o$5S^P2ME=5DhI2H65?CEB_^Hel>u5*l?}7_JpRkTR zQvQzx6sm>o0pl|?-kgA1%n2w<<8R;q<%~Y-C9*{kvYKqZ2dn81h3cazPx`_q-F5l{ zke`k)a(EBJ1up}hLz;ee4&{XQCSJwB;^)<${(=Nm|baAxtWV(B0GeKZEe<*A#r+ebq(Is z_SkQI2vh68p0<4#KxfDfEp)v0nMmZ~#-YJ15NlEBs~17cL$>$ow?}kcL9Lu${uI{( zSb&&7Cfl-JwkIBbkNW_%1%{Kgp3+3o`1EG7XH4N!xK!d*1 zSCBPY3JS^L;5Gn#>X)t=xInpR*SJy|T5ed4D!WC1OOKMLiN4climCSV!{CbFPFcNb z_t)<6o28CUYB~SiA-@xW9HAPzaAxmz=n~+eO@XCQ2*DSLF$D0Ru%?b ze7ojr%|RMJIFdfWXNiuHhk1g0?&In_l!+VVxRa6t+1Eb2fQ*@)sLMnZY z)ZwRhM{e>65_WT6onh1{G!Z1yD7~`MBOV?1?2CB|2E*mo? z=cs7LC_?jCfU$F4-S!Sz##R6GlDX0P>SOr>uGI8i=x%`evJP`qsn758MHp0F|IwcnxfZu%+YCFfrvZL!c`IeHq z&dN7;`hLcamngGHS-VYOyidb*vsoj{b8VXZq4tS7Wf&i=MLVt;*w! z^B&3^^)_pWAJYiU=z>L^YstZ|C1;K>9>RmaquRU14DTrPSli>;&S9;5B=v6a+6+E9 z2Uu8OA?;@eE?|H%B1D5$+RTK1=%7H$xj~`@j9o{>uUW kMAI)6KpC)<%tRy}L@sSz@FNfIDb1Vb;^1a~jpon#Z Date: Mon, 24 Jun 2024 12:04:45 -0600 Subject: [PATCH 093/157] Downloads: performance improvements and merge adapters (#1145) --- .../ui/download/DownloadAdapter.kt | 223 ++++++++++++++ .../ui/download/DownloadButtonSetup.kt | 2 - .../ui/download/DownloadChildAdapter.kt | 94 ------ .../ui/download/DownloadChildFragment.kt | 56 ++-- .../ui/download/DownloadFragment.kt | 289 ++++++++---------- .../ui/download/DownloadHeaderAdapter.kt | 149 --------- .../ui/download/DownloadViewModel.kt | 58 ++-- .../cloudstream3/ui/result/EpisodeAdapter.kt | 36 +-- .../ui/result/ResultFragmentPhone.kt | 24 +- .../ui/result/ResultViewModel2.kt | 46 +-- .../cloudstream3/ui/search/SearchHelper.kt | 20 +- .../cloudstream3/utils/VideoDownloadHelper.kt | 12 +- .../res/layout/download_header_episode.xml | 6 +- 13 files changed, 488 insertions(+), 527 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt new file mode 100644 index 00000000..8f496b3c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -0,0 +1,223 @@ +package com.lagradost.cloudstream3.ui.download + +import android.annotation.SuppressLint +import android.text.format.Formatter.formatShortFileSize +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding +import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.VideoDownloadHelper + +const val DOWNLOAD_ACTION_PLAY_FILE = 0 +const val DOWNLOAD_ACTION_DELETE_FILE = 1 +const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 +const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 +const val DOWNLOAD_ACTION_DOWNLOAD = 4 +const val DOWNLOAD_ACTION_LONG_CLICK = 5 + +abstract class VisualDownloadCached( + open val currentBytes: Long, + open val totalBytes: Long, + open val data: VideoDownloadHelper.DownloadCached +) { + + // Just to be extra-safe with areContentsTheSame + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is VisualDownloadCached) return false + + if (currentBytes != other.currentBytes) return false + if (totalBytes != other.totalBytes) return false + if (data != other.data) return false + + return true + } + + override fun hashCode(): Int { + var result = currentBytes.hashCode() + result = 31 * result + totalBytes.hashCode() + result = 31 * result + data.hashCode() + return result + } +} + +data class VisualDownloadChildCached( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadEpisodeCached, +): VisualDownloadCached(currentBytes, totalBytes, data) + +data class VisualDownloadHeaderCached( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadHeaderCached, + val child: VideoDownloadHelper.DownloadEpisodeCached?, + val currentOngoingDownloads: Int, + val totalDownloads: Int, +): VisualDownloadCached(currentBytes, totalBytes, data) + +data class DownloadClickEvent( + val action: Int, + val data: VideoDownloadHelper.DownloadEpisodeCached +) + +data class DownloadHeaderClickEvent( + val action: Int, + val data: VideoDownloadHelper.DownloadHeaderCached +) + +class DownloadAdapter( + private val clickCallback: (DownloadHeaderClickEvent) -> Unit, + private val mediaClickCallback: (DownloadClickEvent) -> Unit, +) : ListAdapter(DiffCallback()) { + + companion object { + private const val VIEW_TYPE_HEADER = 0 + private const val VIEW_TYPE_CHILD = 1 + } + + inner class DownloadViewHolder( + private val binding: ViewBinding, + private val clickCallback: (DownloadHeaderClickEvent) -> Unit, + private val mediaClickCallback: (DownloadClickEvent) -> Unit, + ) : RecyclerView.ViewHolder(binding.root) { + + @SuppressLint("SetTextI18n") + fun bind(card: VisualDownloadCached?) { + when (binding) { + is DownloadHeaderEpisodeBinding -> binding.apply { + if (card == null || card !is VisualDownloadHeaderCached) return@apply + val d = card.data + + downloadHeaderPoster.apply { + setImage(d.poster) + setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(1, d)) + } + } + + downloadHeaderTitle.text = d.name + val mbString = formatShortFileSize(itemView.context, card.totalBytes) + + if (card.child != null) { + downloadHeaderGotoChild.isVisible = false + + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) + downloadButton.isVisible = true + + episodeHolder.setOnClickListener { + mediaClickCallback.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + card.child + ) + ) + } + } else { + downloadButton.isVisible = false + downloadHeaderGotoChild.isVisible = true + + try { + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format) + .format( + card.totalDownloads, + if (card.totalDownloads == 1) downloadHeaderInfo.context.getString( + R.string.episode + ) else downloadHeaderInfo.context.getString( + R.string.episodes + ), + mbString + ) + } catch (t: Throwable) { + // You probably formatted incorrectly + downloadHeaderInfo.text = "Error" + logError(t) + } + + episodeHolder.setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(0, d)) + } + } + } + + is DownloadChildEpisodeBinding -> binding.apply { + if (card == null || card !is VisualDownloadChildCached) return@apply + val d = card.data + + val posDur = DataStoreHelper.getViewPos(d.id) + downloadChildEpisodeProgress.apply { + if (posDur != null) { + val visualPos = posDur.fixVisual() + max = (visualPos.duration / 1000).toInt() + progress = (visualPos.position / 1000).toInt() + isVisible = true + } else isVisible = false + } + + downloadButton.setDefaultClickListener(card.data, downloadChildEpisodeTextExtra, mediaClickCallback) + + downloadChildEpisodeText.apply { + text = context.getNameFull(d.name, d.episode, d.season) + isSelected = true // Needed for text repeating + } + + downloadChildEpisodeHolder.setOnClickListener { + mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + } + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder { + val binding = when (viewType) { + VIEW_TYPE_HEADER -> { + DownloadHeaderEpisodeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + } + VIEW_TYPE_CHILD -> { + DownloadChildEpisodeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + } + else -> throw IllegalArgumentException("Invalid view type") + } + return DownloadViewHolder(binding, clickCallback, mediaClickCallback) + } + + override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + override fun getItemViewType(position: Int): Int { + val card = getItem(position) + return if (card is VisualDownloadChildCached) VIEW_TYPE_CHILD else VIEW_TYPE_HEADER + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + return oldItem.data.id == newItem.data.id + } + + override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + return oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 10ce67a7..880d5f6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.download -import android.app.Activity import android.content.DialogInterface import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -22,7 +21,6 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { val id = click.data.id - if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return when (click.action) { DOWNLOAD_ACTION_DELETE_FILE -> { activity?.let { ctx -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt deleted file mode 100644 index 1d7b5a83..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual -import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.VideoDownloadHelper - -const val DOWNLOAD_ACTION_PLAY_FILE = 0 -const val DOWNLOAD_ACTION_DELETE_FILE = 1 -const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 -const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 -const val DOWNLOAD_ACTION_DOWNLOAD = 4 -const val DOWNLOAD_ACTION_LONG_CLICK = 5 - -data class VisualDownloadChildCached( - val currentBytes: Long, - val totalBytes: Long, - val data: VideoDownloadHelper.DownloadEpisodeCached, -) - -data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached) - -class DownloadChildAdapter( - var cardList: List, - private val clickCallback: (DownloadClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DownloadChildViewHolder( - DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false), - clickCallback - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is DownloadChildViewHolder -> { - holder.bind(cardList[position]) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - class DownloadChildViewHolder - constructor( - val binding: DownloadChildEpisodeBinding, - private val clickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(binding.root) { - - /*private val title: TextView = itemView.download_child_episode_text - private val extraInfo: TextView = itemView.download_child_episode_text_extra - private val holder: CardView = itemView.download_child_episode_holder - private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress - private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded - private val downloadImage: ImageView = itemView.download_child_episode_download*/ - - - fun bind(card: VisualDownloadChildCached) { - val d = card.data - - val posDur = getViewPos(d.id) - binding.downloadChildEpisodeProgress.apply { - if (posDur != null) { - val visualPos = posDur.fixVisual() - max = (visualPos.duration / 1000).toInt() - progress = (visualPos.position / 1000).toInt() - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } - - binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback) - - binding.downloadChildEpisodeText.apply { - text = context.getNameFull(d.name, d.episode, d.season) - isSelected = true // is needed for text repeating - } - - - binding.downloadChildEpisodeHolder.setOnClickListener { - clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index f54c8698..7734cb08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -40,7 +39,8 @@ class DownloadChildFragment : Fragment() { super.onDestroyView() } - var binding: FragmentChildDownloadsBinding? = null + private var binding: FragmentChildDownloadsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -48,7 +48,7 @@ class DownloadChildFragment : Fragment() { ): View { val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false) + return localBinding.root } private fun updateList(folder: String) = main { @@ -60,7 +60,11 @@ class DownloadChildFragment : Fragment() { }.mapNotNull { val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) ?: return@mapNotNull null - VisualDownloadChildCached(info.fileLength, info.totalBytes, it) + VisualDownloadChildCached( + currentBytes = info.fileLength, + totalBytes = info.totalBytes, + data = it, + ) } }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } if (eps.isEmpty()) { @@ -68,9 +72,7 @@ class DownloadChildFragment : Fragment() { return@main } - (binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList = - eps - binding?.downloadChildList?.adapter?.notifyDataSetChanged() + (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps) } } @@ -98,31 +100,39 @@ class DownloadChildFragment : Fragment() { setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = - DownloadChildAdapter( - ArrayList(), - ) { click -> - handleDownloadClick(click) + val adapter = DownloadAdapter( + {}, + { downloadClickEvent -> + handleDownloadClick(downloadClickEvent) + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + setUpDownloadDeleteListener(folder) + } } + ) + binding?.downloadChildList?.apply { + setHasFixedSize(true) + setItemViewCacheSize(20) + this.adapter = adapter + setLinearListLayout( + isHorizontal = false, + nextRight = FOCUS_SELF, + nextDown = FOCUS_SELF, + ) + } + + updateList(folder) + } + + private fun setUpDownloadDeleteListener(folder: String) { downloadDeleteEventListener = { id: Int -> - val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList + val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList if (list != null) { if (list.any { it.data.id == id }) { updateList(folder) } } } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } - - binding?.downloadChildList?.adapter = adapter - binding?.downloadChildList?.setLinearListLayout( - isHorizontal = false, - nextDown = FOCUS_SELF, - nextRight = FOCUS_SELF - )//layoutManager = GridLayoutManager(context, 1) - - updateList(folder) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 31790b0f..de2d4f3c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -10,14 +10,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import android.widget.TextView import android.widget.Toast +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding @@ -42,11 +43,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI - const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { @@ -63,33 +62,30 @@ class DownloadFragment : Fragment() { private fun setList(list: List) { main { - (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list - binding?.downloadList?.adapter?.notifyDataSetChanged() + (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(list) } } override fun onDestroyView() { - if (downloadDeleteEventListener != null) { - VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! - downloadDeleteEventListener = null + downloadDeleteEventListener?.let { + VideoDownloadManager.downloadDeleteEvent -= it } + downloadDeleteEventListener = null binding = null super.onDestroyView() } - var binding: FragmentDownloadsBinding? = null + private var binding: FragmentDownloadsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - downloadsViewModel = - ViewModelProvider(this)[DownloadViewModel::class.java] - + ): View { + downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false) + return localBinding.root } private var downloadDeleteEventListener: ((Int) -> Unit)? = null @@ -97,7 +93,6 @@ class DownloadFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() - binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() observe(downloadsViewModel.noDownloadsText) { @@ -108,176 +103,148 @@ class DownloadFragment : Fragment() { binding?.downloadLoading?.isVisible = false } observe(downloadsViewModel.availableBytes) { - binding?.downloadFreeTxt?.text = - getString(R.string.storage_size_format).format( - getString(R.string.free_storage), - formatShortFileSize(view.context, it) - ) - binding?.downloadFree?.setLayoutWidth(it) + updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree) } observe(downloadsViewModel.usedBytes) { - binding?.apply { - downloadUsedTxt.text = - getString(R.string.storage_size_format).format( - getString(R.string.used_storage), - formatShortFileSize(view.context, it) - ) - downloadUsed.setLayoutWidth(it) - downloadStorageAppbar.isVisible = it > 0 - } + updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed) + binding?.downloadStorageAppbar?.isVisible = it > 0 } observe(downloadsViewModel.downloadBytes) { - binding?.apply { - downloadAppTxt.text = - getString(R.string.storage_size_format).format( - getString(R.string.app_storage), - formatShortFileSize(view.context, it) - ) - downloadApp.setLayoutWidth(it) - } + updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp) } - val adapter: RecyclerView.Adapter = - DownloadHeaderAdapter( - ArrayList(), - { click -> - when (click.action) { - 0 -> { - if (click.data.type.isMovieType()) { - //wont be called - } else { - val folder = DataStore.getFolderName( - DOWNLOAD_EPISODE_CACHE, - click.data.id.toString() - ) - activity?.navigate( - R.id.action_navigation_downloads_to_navigation_download_child, - DownloadChildFragment.newInstance(click.data.name, folder) - ) - } - } - - 1 -> { - (activity as AppCompatActivity?)?.loadResult( - click.data.url, - click.data.apiName - ) - } - } - - }, - { downloadClickEvent -> - if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - context?.let { ctx -> - downloadsViewModel.updateList(ctx) - } - } - } - ) - - downloadDeleteEventListener = { id -> - val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList - if (list != null) { - if (list.any { it.data.id == id }) { - context?.let { ctx -> - setList(ArrayList()) - downloadsViewModel.updateList(ctx) - } + val adapter = DownloadAdapter( + { click -> + handleItemClick(click) + }, + { downloadClickEvent -> + handleDownloadClick(downloadClickEvent) + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + setUpDownloadDeleteListener() } } - } - - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + ) binding?.downloadList?.apply { + setHasFixedSize(true) + setItemViewCacheSize(20) this.adapter = adapter setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, nextUp = FOCUS_SELF, - nextDown = FOCUS_SELF + nextDown = FOCUS_SELF, ) - //layoutManager = GridLayoutManager(context, 1) } - // Should be visible in emulator layout - binding?.downloadStreamButton?.isGone = isLayout(TV) - binding?.downloadStreamButton?.setOnClickListener { - val dialog = - Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) - - val binding = StreamInputBinding.inflate(dialog.layoutInflater) - - dialog.setContentView(binding.root) - - dialog.show() - - // If user has clicked the switch do not interfere - var preventAutoSwitching = false - binding.hlsSwitch.setOnClickListener { - preventAutoSwitching = true - } - - fun activateSwitchOnHls(text: String?) { - binding.hlsSwitch.isChecked = normalSafeApiCall { - URI(text).path?.substringAfterLast(".")?.contains("m3u") - } == true - } - - binding.streamReferer.doOnTextChanged { text, _, _, _ -> - if (!preventAutoSwitching) - activateSwitchOnHls(text?.toString()) - } - - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt( - 0 - )?.text?.toString()?.let { copy -> - val fixedText = copy.trim() - binding.streamUrl.setText(fixedText) - activateSwitchOnHls(fixedText) - } - - binding.applyBtt.setOnClickListener { - val url = binding.streamUrl.text?.toString() - if (url.isNullOrEmpty()) { - showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) - } else { - val referer = binding.streamReferer.text?.toString() - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - LinkGenerator( - listOf(BasicLink(url)), - extract = true, - referer = referer, - isM3u8 = binding.hlsSwitch.isChecked - ) - ) - ) - - dialog.dismissSafe(activity) - } - } - - binding.cancelBtt.setOnClickListener { - dialog.dismissSafe(activity) - } + binding?.downloadStreamButton?.apply { + isGone = isLayout(TV) + setOnClickListener { showStreamInputDialog(it.context) } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - val dy = scrollY - oldScrollY - if (dy > 0) { //check for scroll down - binding?.downloadStreamButton?.shrink() // hide - } else if (dy < -5) { - binding?.downloadStreamButton?.extend() // show - } + handleScroll(scrollY - oldScrollY) } } downloadsViewModel.updateList(requireContext()) - fixPaddingStatusbar(binding?.downloadRoot) } + + private fun handleItemClick(click: DownloadHeaderClickEvent) { + when (click.action) { + 0 -> { + if (!click.data.type.isMovieType()) { + val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) + activity?.navigate( + R.id.action_navigation_downloads_to_navigation_download_child, + DownloadChildFragment.newInstance(click.data.name, folder) + ) + } + } + 1 -> { + (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName) + } + } + } + + private fun setUpDownloadDeleteListener() { + downloadDeleteEventListener = { id -> + val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList + if (list?.any { it.data.id == id } == true) { + context?.let { downloadsViewModel.updateList(it) } + } + } + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateStorageInfo( + context: Context, + bytes: Long, + @StringRes stringRes: Int, + textView: TextView?, + view: View? + ) { + textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes)) + view?.setLayoutWidth(bytes) + } + + private fun showStreamInputDialog(context: Context) { + val dialog = Dialog(context, R.style.AlertDialogCustom) + val binding = StreamInputBinding.inflate(dialog.layoutInflater) + dialog.setContentView(binding.root) + dialog.show() + + var preventAutoSwitching = false + binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true } + + binding.streamReferer.doOnTextChanged { text, _, _, _ -> + if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) + } + + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy -> + val fixedText = copy.trim() + binding.streamUrl.setText(fixedText) + activateSwitchOnHls(fixedText, binding) + } + + binding.applyBtt.setOnClickListener { + val url = binding.streamUrl.text?.toString() + if (url.isNullOrEmpty()) { + showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) + } else { + val referer = binding.streamReferer.text?.toString() + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + LinkGenerator( + listOf(BasicLink(url)), + extract = true, + referer = referer, + isM3u8 = binding.hlsSwitch.isChecked + ) + ) + ) + dialog.dismissSafe(activity) + } + } + + binding.cancelBtt.setOnClickListener { + dialog.dismissSafe(activity) + } + } + + private fun activateSwitchOnHls(text: String?, binding: StreamInputBinding) { + binding.hlsSwitch.isChecked = normalSafeApiCall { + URI(text).path?.substringAfterLast(".")?.contains("m3u") + } == true + } + + private fun handleScroll(dy: Int) { + if (dy > 0) { + binding?.downloadStreamButton?.shrink() + } else if (dy < -5) { + binding?.downloadStreamButton?.extend() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt deleted file mode 100644 index 65a6441f..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.annotation.SuppressLint -import android.text.format.Formatter.formatShortFileSize -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import java.util.* - -data class VisualDownloadHeaderCached( - val currentOngoingDownloads: Int, - val totalDownloads: Int, - val totalBytes: Long, - val currentBytes: Long, - val data: VideoDownloadHelper.DownloadHeaderCached, - val child: VideoDownloadHelper.DownloadEpisodeCached?, -) - -data class DownloadHeaderClickEvent( - val action: Int, - val data: VideoDownloadHelper.DownloadHeaderCached -) - -class DownloadHeaderAdapter( - var cardList: List, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val movieClickCallback: (DownloadClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DownloadHeaderViewHolder( - DownloadHeaderEpisodeBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - clickCallback, - movieClickCallback - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is DownloadHeaderViewHolder -> { - holder.bind(cardList[position]) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - class DownloadHeaderViewHolder - constructor( - val binding: DownloadHeaderEpisodeBinding, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val movieClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(binding.root) { - - /*private val poster: ImageView? = itemView.download_header_poster - private val title: TextView = itemView.download_header_title - private val extraInfo: TextView = itemView.download_header_info - private val holder: CardView = itemView.episode_holder - - private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded - private val downloadImage: ImageView = itemView.download_header_episode_download - private val normalImage: ImageView = itemView.download_header_goto_child*/ - - @SuppressLint("SetTextI18n") - fun bind(card: VisualDownloadHeaderCached) { - val d = card.data - - binding.downloadHeaderPoster.apply { - setImage(d.poster) - setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(1, d)) - } - } - - binding.apply { - - binding.downloadHeaderTitle.text = d.name - val mbString = formatShortFileSize(itemView.context, card.totalBytes) - - //val isMovie = d.type.isMovieType() - if (card.child != null) { - //downloadHeaderProgressDownloaded.visibility = View.VISIBLE - - // downloadHeaderEpisodeDownload.visibility = View.VISIBLE - binding.downloadHeaderGotoChild.visibility = View.GONE - - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback) - downloadButton.isVisible = true - /*setUpButton( - card.currentBytes, - card.totalBytes, - downloadBar, - downloadImage, - extraInfo, - card.child, - movieClickCallback - )*/ - - episodeHolder.setOnClickListener { - movieClickCallback.invoke( - DownloadClickEvent( - DOWNLOAD_ACTION_PLAY_FILE, - card.child - ) - ) - } - } else { - downloadButton.isVisible = false - // downloadHeaderProgressDownloaded.visibility = View.GONE - // downloadHeaderEpisodeDownload.visibility = View.GONE - binding.downloadHeaderGotoChild.visibility = View.VISIBLE - - try { - downloadHeaderInfo.text = - downloadHeaderInfo.context.getString(R.string.extra_info_format).format( - card.totalDownloads, - if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(R.string.episode) else downloadHeaderInfo.context.getString( - R.string.episodes - ), - mbString - ) - } catch (t: Throwable) { - // you probably formatted incorrectly - downloadHeaderInfo.text = "Error" - logError(t) - } - - - episodeHolder.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(0, d)) - } - } - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 3a74a715..380430e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -39,6 +39,8 @@ class DownloadViewModel : ViewModel() { val availableBytes: LiveData = _availableBytes val downloadBytes: LiveData = _downloadBytes + private var previousVisual: List? = null + fun updateList(context: Context) = viewModelScope.launchSafe { val children = withContext(Dispatchers.IO) { val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE) @@ -53,7 +55,6 @@ class DownloadViewModel : ViewModel() { // parentId : downloadsCount val totalDownloads = HashMap() - // Gets all children downloads withContext(Dispatchers.IO) { for (c in children) { @@ -69,7 +70,7 @@ class DownloadViewModel : ViewModel() { } } - val cached = withContext(Dispatchers.IO) { // wont fetch useless keys + val cached = withContext(Dispatchers.IO) { // Won't fetch useless keys totalDownloads.entries.filter { it.value > 0 }.mapNotNull { context.getKey( DOWNLOAD_HEADER_CACHE, @@ -79,7 +80,7 @@ class DownloadViewModel : ViewModel() { } val visual = withContext(Dispatchers.IO) { - cached.mapNotNull { // TODO FIX + cached.mapNotNull { val downloads = totalDownloads[it.id] ?: 0 val bytes = totalBytesUsedByChild[it.id] ?: 0 val currentBytes = currentBytesUsedByChild[it.id] ?: 0 @@ -91,32 +92,37 @@ class DownloadViewModel : ViewModel() { getFolderName(it.id.toString(), it.id.toString()) ) VisualDownloadHeaderCached( - 0, - downloads, - bytes, - currentBytes, - it, - movieEpisode + currentBytes = currentBytes, + totalBytes = bytes, + data = it, + child = movieEpisode, + currentOngoingDownloads = 0, + totalDownloads = downloads, ) }.sortedBy { (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) - } // episode sorting by episode, lowest to highest - } - try { - val stat = StatFs(Environment.getExternalStorageDirectory().path) - - val localBytesAvailable = stat.availableBytes//stat.blockSizeLong * stat.blockCountLong - val localTotalBytes = stat.blockSizeLong * stat.blockCountLong - val localDownloadedBytes = visual.sumOf { it.totalBytes } - - _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) - _availableBytes.postValue(localBytesAvailable) - _downloadBytes.postValue(localDownloadedBytes) - } catch (t : Throwable) { - _downloadBytes.postValue(0) - logError(t) + } // Episode sorting by episode, lowest to highest } - _headerCards.postValue(visual) + // Only update list if different from the previous one to prevent duplicate initialization + if (visual != previousVisual) { + previousVisual = visual + + try { + val stat = StatFs(Environment.getExternalStorageDirectory().path) + val localBytesAvailable = stat.availableBytes + val localTotalBytes = stat.blockSizeLong * stat.blockCountLong + val localDownloadedBytes = visual.sumOf { it.totalBytes } + + _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) + _availableBytes.postValue(localBytesAvailable) + _downloadBytes.postValue(localDownloadedBytes) + } catch (t: Throwable) { + _downloadBytes.postValue(0) + logError(t) + } + + _headerCards.postValue(visual) + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index e4fd0559..62b1fdd1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -192,15 +192,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.poster, - card.episode, - card.season, - card.id, - card.parentId, - card.rating, - card.description, - System.currentTimeMillis(), + name = card.name, + poster = card.poster, + episode = card.episode, + season = card.season, + id = card.id, + parentId = card.parentId, + rating = card.rating, + description = card.description, + cacheTime = System.currentTimeMillis(), ), null ) { when (it.action) { @@ -343,15 +343,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.poster, - card.episode, - card.season, - card.id, - card.parentId, - card.rating, - card.description, - System.currentTimeMillis(), + name = card.name, + poster = card.poster, + episode = card.episode, + season = card.season, + id = card.id, + parentId = card.parentId, + rating = card.rating, + description = card.description, + cacheTime = System.currentTimeMillis(), ), null ) { when (it.action) { 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 fb5160a7..e185e75d 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 @@ -185,8 +185,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } binding?.resultFullscreenHolder?.isVisible = !isSuccess && isFullScreenPlayer } - - //player_view?.apply { //alpha = 0.0f //ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply { @@ -200,9 +198,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { // fillAfter = true //} //startAnimation(fadeIn) - // } - - + //} } private fun setTrailers(trailers: List?) { @@ -630,15 +626,15 @@ open class ResultFragmentPhone : FullScreenPlayer() { } downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - ep.name, - ep.poster, - 0, - null, - ep.id, - ep.id, - null, - null, - System.currentTimeMillis(), + name = ep.name, + poster = ep.poster, + episode = 0, + season = null, + id = ep.id, + parentId = ep.id, + rating = null, + description = null, + cacheTime = System.currentTimeMillis(), ), null ) { click -> 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 4285feb1..ac6527de 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 @@ -705,13 +705,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, parentId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName, - url, - currentType, - currentHeaderName, - currentPoster, - parentId, - System.currentTimeMillis(), + apiName = apiName, + url = url, + type = currentType, + name = currentHeaderName, + poster = currentPoster, + id = parentId, + cacheTime = System.currentTimeMillis(), ) ) @@ -722,15 +722,15 @@ class ResultViewModel2 : ViewModel() { ), // 3 deep folder for faster acess episode.id.toString(), VideoDownloadHelper.DownloadEpisodeCached( - episode.name, - episode.poster, - episode.episode, - episode.season, - episode.id, - parentId, - episode.rating, - episode.description, - System.currentTimeMillis(), + name = episode.name, + poster = episode.poster, + episode = episode.episode, + season = episode.season, + id = episode.id, + parentId = parentId, + rating = episode.rating, + description = episode.description, + cacheTime = System.currentTimeMillis(), ) ) @@ -2776,13 +2776,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, mainId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName, - validUrl, - loadResponse.type, - loadResponse.name, - loadResponse.posterUrl, - mainId, - System.currentTimeMillis(), + apiName = apiName, + url = validUrl, + type = loadResponse.type, + name = loadResponse.name, + poster = loadResponse.posterUrl, + id = mainId, + cacheTime = System.currentTimeMillis(), ) ) if (loadTrailers) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index 5b943105..66423982 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -25,7 +25,7 @@ object SearchHelper { SEARCH_ACTION_PLAY_FILE -> { if (card is DataStoreHelper.ResumeWatchingResult) { val id = card.id - if(id == null) { + if (id == null) { showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT) } else { if (card.isFromDownload) { @@ -33,15 +33,15 @@ object SearchHelper { DownloadClickEvent( DOWNLOAD_ACTION_PLAY_FILE, VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.posterUrl, - card.episode ?: 0, - card.season, - id, - card.parentId ?: return, - null, - null, - System.currentTimeMillis() + name = card.name, + poster = card.posterUrl, + episode = card.episode ?: 0, + season = card.season, + id = id, + parentId = card.parentId ?: return, + rating = null, + description = null, + cacheTime = System.currentTimeMillis(), ) ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index d1614bc1..30f66f83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -3,17 +3,21 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.TvType object VideoDownloadHelper { + abstract class DownloadCached( + @JsonProperty("id") open val id: Int, + ) + data class DownloadEpisodeCached( @JsonProperty("name") val name: String?, @JsonProperty("poster") val poster: String?, @JsonProperty("episode") val episode: Int, @JsonProperty("season") val season: Int?, - @JsonProperty("id") val id: Int, @JsonProperty("parentId") val parentId: Int, @JsonProperty("rating") val rating: Int?, @JsonProperty("description") val description: String?, @JsonProperty("cacheTime") val cacheTime: Long, - ) + override val id: Int, + ): DownloadCached(id) data class DownloadHeaderCached( @JsonProperty("apiName") val apiName: String, @@ -21,9 +25,9 @@ object VideoDownloadHelper { @JsonProperty("type") val type: TvType, @JsonProperty("name") val name: String, @JsonProperty("poster") val poster: String?, - @JsonProperty("id") val id: Int, @JsonProperty("cacheTime") val cacheTime: Long, - ) + override val id: Int, + ): DownloadCached(id) data class ResumeWatching( @JsonProperty("parentId") val parentId: Int, diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 21f79ca6..a0b64ce3 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -59,12 +59,12 @@ Date: Mon, 24 Jun 2024 18:05:34 +0000 Subject: [PATCH 094/157] Improve tests (#1142) --- .../com/lagradost/cloudstream3/MainAPI.kt | 2 +- .../lagradost/cloudstream3/plugins/Plugin.kt | 1 + .../cloudstream3/plugins/PluginManager.kt | 3 +- .../ui/settings/testing/TestResultAdapter.kt | 50 ++++- .../ui/settings/testing/TestViewModel.kt | 2 +- .../cloudstream3/utils/ExtractorApi.kt | 2 +- .../cloudstream3/utils/TestingUtils.kt | 186 ++++++++++++------ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 2 + 9 files changed, 172 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 07a82583..91da2ed0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -622,7 +622,7 @@ abstract class MainAPI { /**Used for testing and can be used to disable the providers if WebView is not available*/ open val usesWebView = false - /** Determines which plugin a given provider is from */ + /** Determines which plugin a given provider is from. This is the full path to the plugin. */ var sourcePlugin: String? = null open val hasMainPage = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 6b7dc90b..7f08af92 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -67,6 +67,7 @@ abstract class Plugin { * This will contain your resources if you specified requiresResources in gradle */ var resources: Resources? = null + /** Full file path to the plugin. */ var __filename: String? = null /** 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 a30af11c..a5631500 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -18,7 +18,6 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider @@ -518,7 +517,7 @@ object PluginManager { return true } - pluginInstance.__filename = fileName + pluginInstance.__filename = file.absolutePath if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index 83480542..023ecb4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -2,26 +2,31 @@ package com.lagradost.cloudstream3.ui.settings.testing import android.app.AlertDialog import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty +import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.TestingUtils +import java.io.File class TestResultAdapter(override val items: MutableList>) : AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( - ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false) + ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) //LayoutInflater.from(parent.context) // .inflate(R.layout.provider_test_item, parent, false), ) @@ -36,7 +41,8 @@ class TestResultAdapter(override val items: MutableList } + + api.sourcePlugin?.let { path -> + val pluginFile = File(path) + // Cannot delete a deleted plugin + if (!pluginFile.exists()) return@let + + builder.setNegativeButton(R.string.delete_plugin) { _, _ -> + ioSafe { + val success = PluginManager.deletePlugin(pluginFile) + + runOnMainThread { + if (success) { + showToast(R.string.plugin_deleted, Toast.LENGTH_SHORT) + } else { + showToast(R.string.error, Toast.LENGTH_SHORT) + } + } + } + } + } + + builder.show() } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 9e6f8a06..818f1fd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -95,7 +95,7 @@ class TestViewModel : ViewModel() { providers.clear() updateProgress() - TestingUtils.getDeferredProviderTests(scope ?: return, apis, ::println) { api, result -> + TestingUtils.getDeferredProviderTests(scope ?: return, apis) { api, result -> addProvider(api, result) } } 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 1302453a..12b8837a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1015,7 +1015,7 @@ abstract class ExtractorApi { abstract val mainUrl: String abstract val requiresReferer: Boolean - /** Determines which plugin a given extractor is from */ + /** Determines which plugin a given provider is from. This is the full path to the plugin. */ var sourcePlugin: String? = null //suspend fun getSafeUrl(url: String, referer: String? = null): List? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index dd973538..5e2b2bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -13,16 +13,55 @@ object TestingUtils { } } - class TestResultSearch(val results: List) : TestResult(true) - class TestResultLoad(val extractorData: String) : TestResult(true) + class Logger { + enum class LogLevel { + Normal, + Warning, + Error; + } - class TestResultProvider(success: Boolean, val log: String, val exception: Throwable?) : + data class Message(val level: LogLevel, val message: String) { + override fun toString(): String { + val level = when (this.level) { + LogLevel.Normal -> "" + LogLevel.Warning -> "Warning: " + LogLevel.Error -> "Error: " + } + return "$level$message" + } + } + + private val messageLog = mutableListOf() + + fun getRawLog(): List = messageLog + + fun log(message: String) { + messageLog.add(Message(LogLevel.Normal, message)) + } + + fun warn(message: String) { + messageLog.add(Message(LogLevel.Warning, message)) + } + + fun error(message: String) { + messageLog.add(Message(LogLevel.Error, message)) + } + } + + class TestResultList(val results: List) : TestResult(true) + class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) + + class TestResultProvider( + success: Boolean, + val log: List, + val exception: Throwable? + ) : TestResult(success) @Throws(AssertionError::class, CancellationException::class) suspend fun testHomepage( api: MainAPI, - logger: (String) -> Unit + logger: Logger ): TestResult { if (api.hasMainPage) { try { @@ -31,22 +70,33 @@ object TestingUtils { api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages)) when { homepage == null -> { - logger.invoke("Homepage provider ${api.name} did not correctly load homepage!") + logger.error("Provider ${api.name} did not correctly load homepage!") } + homepage.items.isEmpty() -> { - logger.invoke("Homepage provider ${api.name} does not contain any items!") + logger.warn("Provider ${api.name} does not contain any homepage rows!") } + homepage.items.any { it.list.isEmpty() } -> { - logger.invoke("Homepage provider ${api.name} does not have any items on result!") + logger.warn("Provider ${api.name} does not have any items in a homepage row!") } } + val homePageList = homepage?.items?.flatMap { it.list } ?: emptyList() + return TestResultList(homePageList) } catch (e: Throwable) { - if (e is NotImplementedError) { - Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") - } else if (e is CancellationException) { - throw e + when (e) { + is NotImplementedError -> { + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") + } + + is CancellationException -> { + throw e + } + + else -> { + e.message?.let { logger.warn("Exception thrown when loading homepage: \"$it\"") } + } } - logError(e) } } return TestResult.Pass @@ -54,11 +104,13 @@ object TestingUtils { @Throws(AssertionError::class, CancellationException::class) private suspend fun testSearch( - api: MainAPI + api: MainAPI, + testQueries: List, + logger: Logger, ): TestResult { - val searchQueries = listOf("over", "iron", "guy") - val searchResults = searchQueries.firstNotNullOfOrNull { query -> + val searchResults = testQueries.firstNotNullOfOrNull { query -> try { + logger.log("Searching for: $query") api.search(query).takeIf { !it.isNullOrEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { @@ -72,12 +124,11 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - Assert.fail("Api ${api.name} did not return any valid search responses") + Assert.fail("Api ${api.name} did not return any search responses") TestResult.Fail // Should not be reached } else { - TestResultSearch(searchResults) + TestResultList(searchResults) } - } @@ -85,31 +136,27 @@ object TestingUtils { private suspend fun testLoad( api: MainAPI, result: SearchResponse, - logger: (String) -> Unit + logger: Logger ): TestResult { try { - Assert.assertEquals( - "Invalid apiName on SearchResponse on ${api.name}", - result.apiName, - api.name - ) + if (result.apiName != api.name) { + logger.warn("Wrong apiName on SearchResponse: ${api.name} != ${result.apiName}") + } val loadResponse = api.load(result.url) if (loadResponse == null) { - logger.invoke("Returned null loadResponse on ${result.url} on ${api.name}") + logger.error("Returned null loadResponse on ${result.url} on ${api.name}") return TestResult.Fail } - Assert.assertEquals( - "Invalid apiName on LoadResponse on ${api.name}", - loadResponse.apiName, - result.apiName - ) - Assert.assertTrue( - "Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}", - api.supportedTypes.contains(loadResponse.type) - ) + if (loadResponse.apiName != api.name) { + logger.warn("Wrong apiName on LoadResponse: ${api.name} != ${loadResponse.apiName}") + } + + if (!api.supportedTypes.contains(loadResponse.type)) { + logger.warn("Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}") + } val url = when (loadResponse) { is AnimeLoadResponse -> { @@ -117,39 +164,43 @@ object TestingUtils { loadResponse.episodes.keys.isEmpty() || loadResponse.episodes.keys.any { loadResponse.episodes[it].isNullOrEmpty() } if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } (loadResponse.episodes[loadResponse.episodes.keys.firstOrNull()])?.firstOrNull()?.data } + is MovieLoadResponse -> { val gotNoEpisodes = loadResponse.dataUrl.isBlank() if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no movie on ${loadResponse.url}") + logger.error("Api ${api.name} got no movie on ${loadResponse.url}") return TestResult.Fail } loadResponse.dataUrl } + is TvSeriesLoadResponse -> { val gotNoEpisodes = loadResponse.episodes.isEmpty() if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } loadResponse.episodes.firstOrNull()?.data } + is LiveStreamLoadResponse -> { loadResponse.dataUrl } + else -> { - logger.invoke("Unknown load response: ${loadResponse.javaClass.name}") + logger.error("Unknown load response: ${loadResponse.javaClass.name}") return TestResult.Fail } } ?: return TestResult.Fail - return TestResultLoad(url) + return TestResultLoad(url, loadResponse.type != TvType.CustomMedia) // val loadTest = testLoadResponse(api, load, logger) // if (loadTest is TestResultLoad) { @@ -174,7 +225,7 @@ object TestingUtils { private suspend fun testLinkLoading( api: MainAPI, url: String?, - logger: (String) -> Unit + logger: Logger ): TestResult { Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger @@ -182,7 +233,7 @@ object TestingUtils { var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> - logger.invoke("Video loaded: ${link.name}") + logger.log("Video loaded: ${link.name}") Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 @@ -190,7 +241,7 @@ object TestingUtils { linksLoaded++ } if (success) { - logger.invoke("Links loaded: $linksLoaded") + logger.log("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") @@ -200,8 +251,9 @@ object TestingUtils { is NotImplementedError -> { Assert.fail("Provider has not implemented loadLinks()") } + else -> { - logger.invoke("Failed link loading on ${api.name} using data: $url") + logger.error("Failed link loading on ${api.name} using data: $url") throw e } } @@ -212,53 +264,57 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, providers: Array, - logger: (String) -> Unit, callback: (MainAPI, TestResultProvider) -> Unit ) { providers.forEach { api -> scope.launch { - var log = "" - fun addToLog(string: String) { - log += string + "\n" - logger.invoke(string) - } - fun getLog(): String { - return log.removeSuffix("\n") - } + val logger = Logger() val result = try { - addToLog("Trying ${api.name}") + logger.log("Trying ${api.name}") // Test Homepage - val homepage = testHomepage(api, logger).success - Assert.assertTrue("Homepage failed to load", homepage) + val homepage = testHomepage(api, logger) + Assert.assertTrue("Homepage failed to load", homepage.success) + val homePageList = (homepage as? TestResultList)?.results ?: emptyList() // Test Search Results - val searchResults = testSearch(api) + val searchQueries = + // Use the first 3 home page results as queries since they are guaranteed to exist + (homePageList.take(3).map { it.name } + + // If home page is sparse then use generic search queries + listOf("over", "iron", "guy")).take(3) + + val searchResults = testSearch(api, searchQueries, logger) Assert.assertTrue("Failed to get search results", searchResults.success) - searchResults as TestResultSearch + searchResults as TestResultList // Test Load and LoadLinks // Only try the first 3 search results to prevent spamming val success = searchResults.results.take(3).any { searchResponse -> - addToLog("Testing search result: ${searchResponse.url}") - val loadResponse = testLoad(api, searchResponse, ::addToLog) + logger.log("Testing search result: ${searchResponse.url}") + val loadResponse = testLoad(api, searchResponse, logger) if (loadResponse !is TestResultLoad) { false } else { - testLinkLoading(api, loadResponse.extractorData, ::addToLog).success + if (loadResponse.shouldLoadLinks) { + testLinkLoading(api, loadResponse.extractorData, logger).success + } else { + logger.log("Skipping link loading test") + true + } } } if (success) { - logger.invoke("Success ${api.name}") - TestResultProvider(true, getLog(), null) + logger.log("Success ${api.name}") + TestResultProvider(true, logger.getRawLog(), null) } else { - logger.invoke("Error ${api.name}") - TestResultProvider(false, getLog(), null) + logger.error("Link loading failed") + TestResultProvider(false, logger.getRawLog(), null) } } catch (e: Throwable) { - TestResultProvider(false, getLog(), e) + TestResultProvider(false, logger.getRawLog(), e) } callback.invoke(api, result) } diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7c9ccebe..a37dfad2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -88,4 +88,5 @@ #48E484 #ea596e + #FF9800 \ 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 b39006ad..f577d6e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,6 +304,7 @@ Start Failed Passed + Warning Resume -30 +30 @@ -609,6 +610,7 @@ plugins This will also delete all repository plugins Delete repository + Delete plugin Download the list of sites you want to use Downloaded: %d Disabled: %d From 0d40b5ebe3f6a88b2408149e4e44e3de8a8dfe91 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 24 Jun 2024 21:03:09 +0200 Subject: [PATCH 095/157] Translations update from Hosted Weblate (#1042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aaditya Bhandari Co-authored-by: Adrian Hermida Co-authored-by: Akhlak Ur Rahman Co-authored-by: Alexander Svärd Co-authored-by: Anarchydr Co-authored-by: Andre Costa Co-authored-by: Antonio N Co-authored-by: Azgar Co-authored-by: Colgrave Co-authored-by: Dan Co-authored-by: Eji-san Co-authored-by: Ettore Atalan Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com> Co-authored-by: FUTURE Co-authored-by: Fikri Akbar Co-authored-by: Fjuro Co-authored-by: Huzaifah Asif Co-authored-by: Itsmechinmoy Co-authored-by: Jose Delvani Co-authored-by: Konstantinos Tranoudis Co-authored-by: Krisna A. Prayoga Co-authored-by: Luna712 <142361265+Luna712@users.noreply.github.com> Co-authored-by: Marian Turba Co-authored-by: Massimo Pissarello Co-authored-by: Matthaiks Co-authored-by: Milo Ivir Co-authored-by: Mæve Rey Co-authored-by: Naga Co-authored-by: Nicoara Alex Co-authored-by: Nuno Ferreira Co-authored-by: Only1337 Co-authored-by: Pizza Party Co-authored-by: Putra Iskandar Co-authored-by: Qareen Skoll Co-authored-by: Rex_sa Co-authored-by: SeMih Budur Co-authored-by: Semih Co-authored-by: Sufyan Zahoor Jutt Co-authored-by: Waheed Nazir Co-authored-by: Walter H Co-authored-by: Wei-Cheng Yeh (IID) Co-authored-by: amir Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: gallegonovato Co-authored-by: hugoalh Co-authored-by: ngocanhtve Co-authored-by: programutox Co-authored-by: rwi Co-authored-by: stojkovskistefan Co-authored-by: streaming s Co-authored-by: tuan041 Co-authored-by: user0020 <855309c256@gmail.com> Co-authored-by: ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ Co-authored-by: Сергей (MrSabin) Co-authored-by: தமிழ்நேரம் Co-authored-by: 电棍老板 Co-authored-by: 구병우 --- app/src/main/res/values-ajp/strings.xml | 64 +- app/src/main/res/values-ar/strings.xml | 28 +- app/src/main/res/values-as/strings.xml | 624 ++++++++++++++++++ app/src/main/res/values-bg/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 138 +++- app/src/main/res/values-bp/strings.xml | 23 +- app/src/main/res/values-cs/strings.xml | 23 +- app/src/main/res/values-de/strings.xml | 16 +- app/src/main/res/values-el/strings.xml | 109 ++- app/src/main/res/values-es/strings.xml | 41 +- app/src/main/res/values-fr/strings.xml | 33 +- app/src/main/res/values-hi/strings.xml | 19 +- app/src/main/res/values-hr/strings.xml | 414 ++++++------ app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 13 +- app/src/main/res/values-it/strings.xml | 25 +- app/src/main/res/values-iw/strings.xml | 4 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 115 +++- app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 71 +- app/src/main/res/values-ml/strings.xml | 4 +- app/src/main/res/values-ms/strings.xml | 4 +- app/src/main/res/values-my/strings.xml | 4 +- app/src/main/res/values-ne/strings.xml | 49 +- app/src/main/res/values-nl/strings.xml | 4 +- app/src/main/res/values-no/strings.xml | 4 +- app/src/main/res/values-pl/strings.xml | 23 +- app/src/main/res/values-pt/strings.xml | 10 +- app/src/main/res/values-ro/strings.xml | 102 ++- app/src/main/res/values-ru/strings.xml | 10 +- app/src/main/res/values-sk/strings.xml | 26 +- app/src/main/res/values-so/strings.xml | 4 +- app/src/main/res/values-sv/strings.xml | 54 +- app/src/main/res/values-ta/strings.xml | 604 +++++++++++++++-- app/src/main/res/values-tr/strings.xml | 97 +-- app/src/main/res/values-uk/strings.xml | 23 +- app/src/main/res/values-ur/strings.xml | 151 +++-- app/src/main/res/values-vi/strings.xml | 32 +- app/src/main/res/values-zh-rTW/strings.xml | 84 ++- app/src/main/res/values-zh/strings.xml | 24 +- fastlane/metadata/android/as/changelogs/2.txt | 1 + .../metadata/android/as/full_description.txt | 10 + .../metadata/android/as/short_description.txt | 1 + fastlane/metadata/android/as/title.txt | 1 + .../android/el-GR/short_description.txt | 2 +- .../metadata/android/id/full_description.txt | 10 +- fastlane/metadata/android/ur/changelogs/2.txt | 2 +- .../metadata/android/ur/full_description.txt | 12 +- .../metadata/android/ur/short_description.txt | 2 +- .../metadata/android/zh-TW/changelogs/2.txt | 1 + .../android/zh-TW/full_description.txt | 10 + .../android/zh-TW/short_description.txt | 1 + fastlane/metadata/android/zh-TW/title.txt | 1 + 54 files changed, 2554 insertions(+), 587 deletions(-) create mode 100644 app/src/main/res/values-as/strings.xml create mode 100644 fastlane/metadata/android/as/changelogs/2.txt create mode 100644 fastlane/metadata/android/as/full_description.txt create mode 100644 fastlane/metadata/android/as/short_description.txt create mode 100644 fastlane/metadata/android/as/title.txt create mode 100644 fastlane/metadata/android/zh-TW/changelogs/2.txt create mode 100644 fastlane/metadata/android/zh-TW/full_description.txt create mode 100644 fastlane/metadata/android/zh-TW/short_description.txt create mode 100644 fastlane/metadata/android/zh-TW/title.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 734d5644..554fae9c 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -76,7 +76,7 @@ سَتِنگز الترجمة لون العلبة عمول ستريم للتورنت - تلقائيًا نَزِل كل الإضافات من الريپويات يلي نزادِت. + تلقائيًا نَزِل كل الإضافات من الريپويات يللي نزادِت. محي بلش في تِلِفونات ما فيا تعوز الطريقة الجديدة لتجديد الآپات. جربو \"الطريقة القديمة\" إذا ما عم تنزل التجديدات. @@ -90,7 +90,7 @@ متصفح الوَب كبوس مرتين على اليمين أو الشمال حتى تقرب أو ترَجِع الڤيديو ما نلاقى وصف الأحداث - الحلقة يلي بَعدا + الحلقة يللي بَعدها فرجي تجديدات الآپ رفّ آپ من نفس المطورين للروايات الخفيفة، بدل من الڤيديوات @@ -218,7 +218,7 @@ بث مباشر المشغل مبين - مدة التقديم مشكلة مش متوقع بمشغل الڤيديو (Unexpected player error) - بسبب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يلي ما بتساع كتير، متل تلفزيون \"أندرويد\". + بسبِب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يللي ما بتساع كتير، متل تلفزيون \"أندرويد\". شي غير أفي هيدا التجديد نسوخ الرابط @@ -244,7 +244,7 @@ طول التخزين المتوقت حلقة \"كروم كاست\" دراما آسيوية - بسبب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يلي ذِكرتا زغيرة، متل تلفزيون \"أندرويد\". + بسبِب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يللي ذِكرتها زغيرة، متل تلفزيون \"أندرويد\". مشكلة بالمصدر التخزين الموقت للڤيديو على الديسك فلم وثائقي @@ -283,7 +283,7 @@ إشارات المرجعية بَلَش التنزيل فتّو على الأكونت \"%s\" - وقِف الإعلان الأتوماتيكي عن المشاكل يلي بالآپ + وقِف الإعلان الأتوماتيكي عن المشاكل يللي بالآپ محل عنوان الپوستر الشكل %1$d ساعة %2$d ديقة @@ -314,7 +314,7 @@ مدبلج أوتوماتيك عدلو الأكونت - الأرقام السرية يلي نحطت مش صحيحة. جرب مرة أخرى. + الأرقام السرية يللي نحطت مش صحيحة. جرب مرة أخرى. الشكل المعمول للتلفزيون نلغى التنزيل أكونت @@ -366,9 +366,9 @@ زِدت %s المفضلين \"%s\" نزاد ع المفضل - العشوائي يلي بعدو + العشوائي يللي بعده خيال - عم نجدِد المثلثلات يلي مشتركينلا + عم نجدِد المثلثلات يللي مشتركينلها مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: \n \n%s @@ -481,7 +481,7 @@ نزلو المصادر بالجملة فتاح بـ راينيگ - محي الريپو كمان بمحي الإضافات يلي في + محي الريپو كمان بمحي الإضافات يللي فيه اللغة شترك نمحت الإضافة @@ -490,7 +490,7 @@ الإضافات شيل الإعلانات من الترجمة رفّكن فاضي ☹ -\nفوتو على أكونت فيا رفّ الڤيديوات يلي حضرينا أو زيدو ڤيديوات بالرفّ المحلي. +\nفوتو على أكونت فيا رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. إسم الريپوزيتاري الجودات بيانات مش صالحة @@ -530,13 +530,13 @@ Blu-ray %d/10 النص كبير كتير. ما فينا ننسخو. - عدد الإضافيات يلي تجددت: %d + عدد الإضافيات يللي تجددت: %d رح يتجدد الآپ وقتا تطلعو مِنو پلاي ليست \"ايش أل أس\" زيد ريپوزيتوري علم إنو حضرتو شو بَدَك تشوف - شيل المعلومات يلي محطوطة بالترجمة ليلي عندن فقد سمعي + شيل المعلومات يللي محطوطة بال ترجمة ل يللي عندهن فقد سمعي ما لقينا ريپو. تأكدو إنو الرابط صح وجربو تعوزو \"ڤي پي أن\" (VPN) فشل الفوت ع الأكونت \"%s\" الحد الأعلى @@ -544,7 +544,7 @@ إضافات ما قدرنا نفتح %s رايتينگ: %s - بدكن تنزلو كل الإضافات من هيدا الريپو؟ + تحزير: \"كلاود ستريم 3\" مش مسؤولة عن الإضافات المش رسمية، وما بتدعمن أبدًا! الحالة محي الريپو مشغل الڤيديو @@ -567,8 +567,8 @@ الجودة عين الافتراضي المرجع (إختياري) - المشغل يلي بـ\"كلود ستريم\" - نزل لايحة المواقع يلي بدك تعوزن + المشغل يللي ب \"كلود ستريم\" + نزل لايحة المواقع يللي بدك تعوزهن حطو الأرقام السرية فَلتِر حسب اللغة المفضلة أكيد بدكون تطلعو؟ @@ -576,13 +576,13 @@ @string/home_play شيلو من لايحة المحتوى الحاضرينو الإعتمادات - فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يلي عندو أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يلي بتفضلوّا. + فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. \n -\nمثلًا: -\nإذا المصدر \"أ\" بتفضلوّ، بتعطوّ كتير نقات (مثلًا 8). -\nإذا الجودة 480 ما بتحبوّا، بتعطوّا نقات قليلة (مثلًا 1). +\nمتلًا: +\nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). +\nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). \n -\nعلامت المصدر والجودة تبعو بينجمعو مع بعض (8 + 1 = 9). يلي علامتو 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! +\nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! حطو الأرقام السرية الحالية صوت حط كبسة لبرم إتجاه الشاشة @@ -603,7 +603,7 @@ قفل بواسطة المقاييس الحيوية رمز/كلمة مرور للمصادقة فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، إو الپاسورد. - تسَكرت هيدي الواجهة من ورا محاولات فاشلة عديدة. پليز، سكر الآپ ورجاع فتحه. + بعد كذا محاولة فاشلة، هيدا الشباك رح يسكر. بكل بساطة، سكر الآپ ورجاع فتحه حتى تجرب بعد مرة. %s \nباقي المصادقة البيومترية مش مدعومة ع هالجهاز @@ -621,5 +621,23 @@ موسيقى أوديو بوك الميديا - لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. - + ت تضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يللي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي ب الباكگراوند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". +\nملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآپ بال باكگراوند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ ب «الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. + ريسات + رح ينزل ب %s + الحلقة ال %2$d من الجزء ال%1$d رح تنزل ب + كاست مراية + إف كاست + نقي جهاز الكاست + ويكي \"كلود ستريم\" + أكونتات + سكوريتي + صورة الـ\"كيو آر\" كود + فشلنا ب فتح پِن الجهاز، جرب تفوت ع أكونت محليًا + خلصت مدة الپِن! + بتخلص مدة الپِن ب %1$d دقايق و%2$d ثانية + فوت عال أكونت محليًا + تجاهل + متاح الريپوزيتوري + فتاح %s ع تلفونك أو كمپيوترك، وحط الكود اللي فوق + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8681398d..e253ed93 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -220,7 +220,7 @@ أوفا تورنت وثائقي - دراما آسيوية + الدراما الآسيوية بث حي +18 فيديو @@ -435,7 +435,7 @@ عرض مستودعات المجتمع قائمة عامة جميع الترجمات حروف كبيرة - تحميل جميع الإضافات من هذا المستودع\? + تحذير: لا يتحمل CloudStream 3 أي مسؤولية عن استخدام ملحقات الطرف الثالث ولا يقدم أي دعم لها! %s (معطل) المسارات مسار الصوت @@ -631,7 +631,7 @@ افتح التطبيق باستخدام بصمة الإصبع ومعرف الوجه ورقم التعريف الشخصي والنمط وكلمة المرور. فتح سحابة البث مصادقة كلمة المرور/رقم التعريف الشخصي - تم إغلاق هذه الشاشة بسبب عدة محاولات فاشلة. الرجاء إعادة تشغيل التطبيق. + بعد عدة محاولات فاشلة، سيتم إغلاق المطالبة. ما عليك سوى إعادة تشغيل التطبيق للمحاولة مرة أخرى. لقد تم الآن نسخ بيانات CloudStream احتياطيًا. على الرغم من أن احتمال حدوث ذلك منخفض جدًا، إلا أن جميع الأجهزة يمكن أن تتصرف بشكل مختلف. في الحالات النادرة، التي يتم فيها منعك من الوصول إلى التطبيق، قم بمسح بيانات التطبيق بالكامل واستعادتها من نسخة احتياطية. نحن نأسف جدًا لأي إزعاج ناتج عن هذا. %s \nمتبقي @@ -646,8 +646,24 @@ غير قادر على فتح معلومات تطبيق CloudStream. كتاب صوتي حسناً - لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى استخدام بطارية التطبيق -\nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. + لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 واضبط استخدام البطارية على 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الامتدادات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. موسيقى الوسائط - + اعادة تعيين + قادم خلال %s + سيتم إصدار الحلقة %1$d من الموسم %2$d في + مرآة البث + بث ف + حدد جهاز البث + CloudStream ويكي + إعدادات الأمان + الحسابات + صورة رمز الاستجابة السريعة + تجاهَل + فتح المستودع + لقد انتهت صلاحية الرمز السري الآن! + تحقق محليا + قم بزيارة %s على هاتفك الذكي أو جهاز الكمبيوتر وأدخل الرمز أعلاه + لا يمكن الحصول على رمز PIN للجهاز، حاول المصادقة المحلية + تنتهي صلاحية الرمز خلال %1$dm %2$ds + \ No newline at end of file diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml new file mode 100644 index 00000000..8d151d71 --- /dev/null +++ b/app/src/main/res/values-as/strings.xml @@ -0,0 +1,624 @@ + + + %1$s এপি %2$d + পোস্টাৰ + এপিস\'দ পোস্টাৰ + মুখ্য পোস্টাৰ + পিছলৈ যাওক + ৰেটিং: %.1f + ছেটিংছ + অনুসন্ধান কৰক… + %s ত অনুসন্ধান কৰক… + ডাউনল\'ড কৰি আছে + পৰামৰ্শ চাওক + প্লেয়াৰৰ গতি + সাবটেক্সটৰ ৰং + এজেৰ ধৰণ + চালনালৈ অবন্ধ কৰক + প্লট + লগ ক্যাট চাওক + লগ + বেকআপ ত্ৰুটি %s + অনুসন্ধান + গ্ৰন্থাগাৰ + হিসাব আৰু সুৰক্ষা + উন্নয়ন আৰু বেকআপ + স্বয়ংক্ৰিয়ভাৱে প্লাগইন ডাউনল\'ড কৰক + প্লাগইন ডাউনল\'ড ম’ড নিৰ্বাচন কৰক + নিষ্ক্ৰিয়ত আৰম্ভকৰ্তাৰ আদৰ্শ সংগ্ৰহসমূহতৰ প্ৰয়াস হৈছে, আইন্সটল স্বয়ংক্ৰিয়ভাৱে যোগ কৰা হয়। + কাৰ্টুনসমূহ + এনিমে + টৰেণ্টসমূহ + OVA + এছিয়ান ড্ৰামা + লাইভস্ট্ৰিম + NSFW + অন্যান্য + এপত বাজাওক + লিংকসমূহ পুনঃ ল\'ড কৰক + সাবটাইটেল ডাউনল\'ড কৰক + HD চিহ্ন + শিৰোনাম + কোনো উন্নীতকৰণ পোৱা নাই + উন্নীতকৰণ পৰীক্ষা কৰক + আৰম্ভ কৰক না + ভিডিঅ\' প্লেয়াৰৰ শিৰোনামৰ সীমা + বিদ্যমান চাবৰ এটা ক্লোন যোগ কৰক, একটু ভিন্ন urlৰ সৈতে + লিংকসমূহ + আউটলৈন + স্বয়ংক্ৰিয় + password123 + ব্যৱহাৰকাৰীনাম + hello@world.com + 127.0.0.1 + লগ আউট + সমন্বয় সংখ্যা + %d / 10 + /?? + /%d + %s প্ৰমাণিত + %s ত লগ ইন কৰিব পৰা নহয় + অবৈধ ডেটা + অবৈধ url + ত্ৰুটি + সাবটাইটেলৰ পৰা ক্যাপশনসমূহ দূৰ কৰক + উল্লেখক (ঐচ্ছিক) + পৰৱৰ্তী + প্ৰদাতাৰ ভাষা পছন্দ কৰক + গভেয়ান ডাউনলোড + সেটআপ এক্সটেনশনসমূহ + ডাউনলোড হৈছিল: %d + ক্লিপবোৰ্ড অনুমতি দিবলৈ ত্ৰুটি, অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক। + বৰ্ণানুক্রমিক (জ থকা অ পৰা অ) + %d প্ৰকাৰ মুকলো! + সাবস্ক্ৰাইব কৰক + সাবস্ক্ৰাইব পৰা মুকা + প্ৰ’ফাইল %d + পিন এন্টাৰ কৰক + একাউন্ট সম্পাদনা কৰক + মিডিয়া + পুনৰায় ছেট কৰক + অভিনেতা: %s + দেখা হৈ আছে + অবিৰাম দেখিব + সাবটাইটেল + চিত্ৰমূল্যৰ সিষ্টেম ব্যৱহাৰ কৰক + বেকআপ অনুমতি অনুপলব্ধ। অনুগ্ৰহ কৰি পুনৰায় চেষ্টা কৰক। + অল্প + এচডি + সমাপ্ত + সাৰদাৰী ভাণ্ডা + অব্যাহত্‌ + VLCত বাজাওক + বেটাৰী অপটিমাইজেশন অসমৰ্থ + সাবস্ক্ৰাইব কৰা বস্তুসমূহ + মান + যোগ কৰক + পুনৰ্স্থাপন কৰক + ক্লাউডষ্ট্ৰীম অনলক কৰক + বায়োমেট্ৰিক ছেটিংসমূহ + পাছৱাৰ্ড/PIN পুনৰ্প্ৰমাণৰ + সংগীত + অডিঅ’ বুক + পৰৱৰ্তী পৰ্ব %d + পৰৱৰ্তী মৌচৰা %1$d পৰ্ব %2$d + %1$dd %2$dh %3$dm + %1$dh %2$dm + %dm + পোস্টাৰ + প্ৰদাতা সলনি কৰক + পূৰ্বালোচনাৰ পৃষ্ঠপুতী + পৰৱৰ্তী যিনিমূলক + গতি (%.2fx) + পৰৱৰ্তী পৰ্ব + %d মিনিট + ক্লাউডষ্ট্ৰীম + মূল + কোনো ডাটা নাই + অধিক বিকল্পসমূহ + শেয়াৰ কৰক + ব্ৰাউজাৰত খোলক + ব্ৰাউজাৰ + দেখিব প্লেন কৰা হৈছে + টৰেণ্ট ষ্ট্ৰীম কৰক + সংযোগ পুনঃচেষ্টা কৰক… + পিছলৈ যাওক + উৎস বাছনি কৰক + ডাউনলোড হোৱা + ডাউনলোড থমা কৰা হৈছে + ডাউনলোড আৰম্ভ হোৱা + ডাউনলোড বিফল হৈছে + ডাউনলোড বাতিল হৈছে + ডাউনলোড সম্পন্ন হৈছে + আপডেট আৰম্ভ হোৱা + নেটৱৰ্ক ষ্ট্ৰীম + লিংক লোড কৰা বিফল + লিংক পুনৰাবৃত্তি হোৱা + অভ্যন্তৰীণ ষ্ট’ৰেজ + ডাব + ছাবটাইটেল + ফাইল মুছি দিব + ফাইল প্লে কৰক + স্বয়ংক্ৰিয় বাগৰ প্ৰতিবেদন নিষ্ক্ৰিয় কৰক + ডাউনলোড পুনৰাৰম্ভ কৰক + ডাউনলোড থমা কৰক + অধিক তথ্য + লুকুৱা কৰক + খেলা + তথ্য + অপশনটো সংগ্ৰহ বাদ কৰক + সংগ্ৰহৰ তলত যোগ কৰক + সংগ্ৰহৰ ফিল্টাৰ কৰক + সংগ্ৰহ ত্ৰুটি + কপি কৰক + বন্ধ কৰক + পৰিষ্কাৰ কৰক + সংৰক্ষণ কৰক + ভৰসৰ্বক্ষণৰ নাম আৰু URL + প্ৰতিলিপি কৰা হৈছে! + নতুন সদস্যতা + অন্যান্য এক্সটেনছনসমূহত অনুসন্ধান কৰক + সাবটাইটেল ছেটিংছ + আউটলাইনৰ ৰং + পৃষ্ঠপুতীৰ ৰং + উইণ্ডোৰ ৰং + সাবটাইটেলৰ উঁচুতি + ফন্ট + ফন্ট আকাৰ + প্ৰকাৰসমূহে অনুসন্ধান কৰক + সৰব সাধনসমূহে অনুসন্ধান কৰক + %d ডেভল\'পাৰ পৰা বেনেন দিয়া হৈছে + কোনো বেনেন দিয়া নাই + স্বয়ংক্ৰিয় ভাষা বাছনি কৰক + ভাষা ডাউনলোড কৰক + ছাবটাইটেলৰ ভাষা + ডিফল্টৰ পুনৰাৰম্ভ কৰিবলৈ ধাৰণ কৰক + এই প্ৰদাতাৰ সঠিকভাবে কাম কৰিবলৈ এটা VPN প্ৰয়োজন হবে + এই প্ৰদাতা এটা ট\'ৰেণ্ট হৈছে, এটা VPN পৰামৰ্শ দিয়া হল + ফন্ট ইম্প\'ৰ্ট কৰিবলৈ %sত ত থকা প্ৰতিষ্ঠান কৰক + অধিক তথ্য + \@string/home_play + মেটাডাটা ঠিকানাৰ সৈতে প্ৰদান কৰা নাই, যদি এটা ঠিকানাৰ মেটাডাটা নাই তেন্তি ভিডিঅ\' ল\'ড হোৱা নাই। + কোনো বিবৰণ পোৱা নাই + কোনো বিৱৰণ পোৱা নাই + প্লেয়াৰৰ আকাৰ বটাম + কৃষ্টি বোৰ্ডাৰ প্ৰস্তুতি কৰক + ছবি-মাধ্যমে ছবি + অন্য এপ্‌সমূহৰ ওপৰত এটা সন্নিহিত প্লেয়াৰত অবিৰত প্লে কৰা হৈছে + প্লেয়াৰৰ ছাবটাইটেল ছেটিংছ + ক্ৰ\'মকাস্ট ছাবটাইটেল + ক্ৰ\'মকাস্ট ছাবটাইটেল ছেটিংছ + প্লেবেক গতি + প্লেয়াৰত গতিৰ বিকল্প যোগ কৰে + সোধক হওৱাৰ বাবে স্বাইপ কৰক + ভিডিঅ\' ত আপোনাৰ অৱস্থান নিয়ন্ত্ৰণ কৰিবলৈ পাছত থকা দিশত স্বাইপ কৰক + ছেটিংছ সলনী কৰিবলৈ স্বাইপ কৰক + প্ৰকাৰ বা আওতাৰ স্থানত ওপৰত অথবা তলত স্বাইপ কৰক উজ্জ্বলতা অথবা ভলিউম সলনী সলন কৰিবলৈ + স্বয়ংক্ৰিয় পৰবৰ্তী সংযোগ + বর্তমান এটা শেষ হোৱা সময় পৰবৰ্তী সংযোগ আৰম্ভ কৰা + দ্বিগুন টেপ কৰি সন্ধান কৰক + দ্বিগুন টেপ কৰি থম কৰক + প্লেয়াৰ সন্ধান পৰিমাণ (ছেকেন্ড) + অগ্ৰগামী বা পিছত সন্ধান কৰিবলৈ সোহদৰ বা বাঁয়া দিকত দুইবাৰ টেপ কৰক + থমবা নিছবা পৰিমাণ দুইবাৰ টেপ কৰক + অ্যাপ প্লেয়াৰত সিষ্টেমৰ আলোক অভিলাই ব্যৱহাৰ কৰিবলৈ বিষ্টা উলঙ্ঘনস্থিতি ব্যৱহাৰ কৰক + এপিস\'ড সংমিলিত কৰা + আপোনাৰ বৰ্তমান পৰ্বৰ প্ৰগতি স্বয়ংক্ৰিয়ভাবে সমতলীয়া কৰা + বেকআপৰ তথ্য পুনৰায় স্থাপন কৰক + তথ্য বেকআপ কৰক + বেকআপ সংখ্যা + ফাইল %sৰ পৰা তথ্য পুনৰায় স্থাপন কৰা নহয় + তথ্য সংৰক্ষিত হৈছে + তথ্য + বেকআপ ফাইল ল\'ড হৈছে + সুধাৰি অনুসন্ধান + প্ৰদানকাৰীৰ পৰা সন্ধান ফলাফল সোমোৱা + কেৱল দুৰ্ঘটনাত ডাটা পঠাৱ + কোনো ডাটা পঠাৱ নহয় + অনুপ্রেৰণাৰ অধ্যাপক দেখাওঁক + ট্ৰেলাৰ দেখাওঁক + Kitsuৰ পোষ্টাৰ দেখাওঁক + অনুসন্ধান ফলাফলত নিৰ্বাচিত ভিডিঅ’ গুণত্ব লুকাওক + স্বয়ংক্ৰিয় প্লাগইন উন্নয়ন + এপ্লিকেশন উন্নয়ন দেখাওঁক + সেটআপ প্ৰক্ৰিয়া পুনৰাবৃত্তি কৰক + প্ৰি-ৰিলিজ আপডেট কৰক + APK Installer + কিছু ফ’নসমূহ নতুন পেকেজ ইনষ্টলাৰ সমৰ্থন কৰে নাই। আপডেট ইনষ্টল নহয় পৰা পৰীক্ষা কৰক লেগেচি বিকল্প প্ৰয়োগ কৰক। + গিটহাব + একেই ডেভল\'পাৰকৰ লাইট নভেল এপ্‌ + ডিসকৰ্ডত যোগদান কৰক + কোনো লিংক পোৱা নাই + ডেভল\'পাৰকলৈ এখন বিনম্রতা দাওঁক + দিয়া বিনম্রতা + এপ্‌ ভাষা + এই প্ৰদাতাৰ কোনো চ্ৰোমকাছ্ট সমৰ্থন নাই + লিংক ক্লিপবৰ্ডত প্ৰতিলিপি কৰা হৈছে + এপিস\'ড বাজাওঁক + দুঃখিত, অ্যাপ্লিকেশন সংঘটিত হৈছে। অজ্ঞাত বাগ ৰিপ’ৰ্ট ডেভেলপাৰসমূহলৈ পঠাওক + ঋতু + ডিফ’ল্ট মান পুনৰাবৃত্তি কৰক + %1$s %2$d%3$s + কোনো ঋতু নাই + এপিস\'ড + এপিস\'ডসমূহ + %1$d-%2$d + %1$d %2$s + %sত আগবাঢ়িছে + ঋতু + এপিস\' + কোনো এপিস\'ড পোৱা নাই + ফাইল মচি দিব + মচি দিব + বাতিল কৰক + অধিপ্ত কৰক + আৰম্ভ কৰক + অসফল + সফল + আবাৰ আৰম্ভ কৰক + -৩০ + +৩০ + এইটো স্থায়ীভাৱে %s মচি দিব +\nআপুনি নিশ্চিত? + %dm +\nবাকি আছে + %s +\nবাকি আছে + চলিত আছে + সম্পন্ন হৈছে + অবস্থা + বছৰ + ৰেটিং + সময় + চাইট + সাৰাংশ + অপেক্ষাৰত + সাবটাইটেল নাই + ডিফ’ল্ট + মুক্ত + ব্যৱহাৰ কৰা + এপ্‌ স্টোৰেজ + চলচ্চিত্ৰসমূহ + টিভি চলচ্চিত্ৰসমূহ + ডকুমেণ্টাৰিসমূহ + এছিয়ান ড্ৰামা + লাইভস্ট্ৰিমসমূহ + এনএসএফডবলিউ + অন্যান্য + চলচ্চিত্ৰ + সিৰিজ + কাৰ্টুন + অ্যানিমে + OVA + টৰেণ্ট + ডকুমেণ্টাৰী + উৎস ত্ৰুটি + দূৰবৰ্তী ত্ৰুটি + ৰেন্ডাৰাৰ ত্ৰুটি + ডাউনল\'ড ত্ৰুটি, সংৰক্ষণ অনুমতিসমূহ পৰীক্ষা কৰক + অপ্ৰত্যাশিত প্লেয়াৰ ত্ৰুটি + চ্ৰোমকাছ্ট এপিস\'ড + চ্ৰোমকাছ্ট আইনমিৰৰ + আইনমিৰ আদৰণি + ডাব চিহ্ন + %sত বাজাওক + ব্ৰাউজাৰত বাজাওক + লিংক প্ৰতিলিপি কৰক + স্বয়ংক্ৰিয় ডাউনল\'ড + আদৰণি ডাউনল\'ড কৰক + উপশিৰোনিৰ চিহ্ন + প\'ষ্টাৰৰ UI উপাদানসমূহ ট\'গল কৰক + ল\'ক + ৰিসাইজ + উৎস + আপডেট + পছন্দসই দেখাৰ গুণত্ব (WiFi) + অপ স্কিপ কৰক + এই আপডেটটো প্ৰস্থান কৰক + পছন্দসই দেখাৰ গুণত্ব (মোবাইল ডেটা) + ভিডিঅ\' প্লেয়াৰৰ আৰুণিমা + ভিডিঅ\' বাফাৰৰ আকাৰ + ভিডিঅ\' বাফাৰৰ লম্বা + ডিস্কত ভিডিঅ\' কেচ কৰক + ভিডিঅ\' আৰু চিত্ৰ কেচ মুক্ত কৰক + প্লেয়াৰ দৃশ্যমান হ\'লে ব্যৱহাৰ কৰা সন্ধান পৰিমাণ + প্লেয়াৰ লুকুৱাওলৈ - সন্ধান পৰিমাণ + প্লেয়াৰ দেখা হোৱালৈ - সন্ধান পৰিমাণ + প্লেয়াৰ লুকুৱা হ\'লে ব্যৱহাৰ কৰা সন্ধান পৰিমাণ + যদি স্তন্যপান উচ্চত নিৰ্ধাৰিত হ\'লে, অন্যত্ৰ সঞ্চয় স্থানত সন্দেহ ঘটিব। উদাহৰণস্বৰূপ, এছিয়ান টিভি। + DNS ওভাৰ HTTPS + যদি স্তন্যপান উচ্চত নিৰ্ধাৰিত হ\'লে, অন্যত্ৰ মেম\'ৰী সন্ধানৰ যন্ত্ৰসমূহত সন্দেহ ঘটিব। উদাহৰণস্বৰূপ, এছিয়ান টিভি। + আইএছপি ব্লক পাৰ কৰিবলৈ কাৰ্যকাৰী + চাব চাওঁক + GitHub প্ৰক্সি + GitHub প্ৰাপ্য নাই। jsDelivr প্ৰক্সি অন কৰা হচ্ছে… + jsDelivr ব্যৱহাৰ কৰি সোধ গুগল url ব্লক অনলাইন কৰক। কিছুদিনৰ মাজত আপডেট ল\'ওক দিব পাৰে। + চাব আঁতৰাওক + ডাউনল\'ড পাথ + NGINX চাৰ্ভাৰ url + ডাব কৰা/সাব কৰা অ্যানিমে প্ৰদৰ্শন কৰক + স্ক্ৰীনলৈ সজাব কৰক + পৰবৰ্তী কৰক + অকৃত্রিম নোটিশ + আইএছপি পাৰ কৰা + জুম কৰক + এপ্ আপডেটসমূহ + বেকাপ + এক্সটেনচনসমূহ + ক্ৰিয়াসমূহ + কেচ + এণ্ড্ৰোইড টিভি + হাস্য + প্লেয়াৰৰ বৈশিষ্ট্য + উপশিৰোতা + লে\'আ\'উট + ডিফ’ল্টসমূহ + দেখুৱাই + বৈশিষ্ট্যসমূহ + সাধাৰণ + যিতা বুটাম + হোমপেজ আৰু লাইব্ৰেৰিত যিতা বুটাম দেখোৱা + সুপালৈক ভাষাসমূহ + এপ্ লে\'আ\'উট + পছন্দসই মিডিয়া + সুবিধা দিয়া সময় NSFW সক্ষম কৰক + উপশিৰোতা ক\'ডিং + প্ৰদাতা + প্ৰদাতা পৰীক্ষা + সকলো এক্সটেনচনসমূহ পৰীক্ষা কৰক + এই পৰীক্ষা কেৱল উন্নীতকাৰীসমূহলৈ পৰমিট আৰু বাৰ্তা কৰা হৈছে আৰু কেইকোনো এক্সটেনচনৰ কাৰ্যক্ষম বা অকাৰ্যক্ষমতা প্ৰমাণিত নহয়। + টিভি লে\'আ\'উট + এমুলেটৰ লে\'আ\'উট + প্ৰাথমিক ৰং + এপ্ থীম + প\'ষ্টাৰ শিৰোনাম অৱস্থান + ফোন লে\'আ\'উট + প\'ষ্টাৰ তলত শিৰোনাম দিয়ক + নতুন সাইটৰ নাম + https://example.com + ভাষা ক\'ড (en) + %1$s %2$s + একাউন্ট + লগ ইন + একাউন্ট পৰিবৰ্তন কৰক + একাউন্ট যোগ কৰক + একাউন্ট সৃষ্টি কৰক + ট্ৰেকিং যোগ কৰক + কোনোবিলাক + সাধাৰিত + %s যোগ কৰা হৈছে + সিঙ্ক + অক্ষম কৰক + সকল + অধিকতম + আউটলাইন + ডিপ্ৰেছড + ছাড়া + 1000 মিলিছেকেন্ড + উঁচুহোৱা + উপশিৰোতা সিংক + উপশিৰোতা দেলাই + যদি উপশিৰোতা %d মিলিছেকেন্ড পূৰ্ববৰ্তী দেখা যায় তেন্ত এইটো ব্যৱহাৰ কৰক + কোনো উপশিৰোতা বিলম্ব নাই + যদি উপশিৰোতা %d মিলিছেকেন্ড পূৰ্ববৰ্তী দেখা যায় না তেন্ত এইটো ব্যৱহাৰ কৰক + দ্রুত মাৰেল ভুঁটি সোমালী কুকুৰা ধীৰে + অনুমোদিত + %s লোড কৰা হৈছে + ফাইলৰ পৰা লোড কৰক + ইন্টাৰনেটৰ পৰা লোড কৰক + পছৱা + ডাউনলোড কৰা ফাইল + উৎস + প্ৰধান + যাত্রা + শীঘ্ৰই আসিব… + সহায়ক + ক্যাম + ক্যাম + ক্যাম + এইচকিউ + এইচডি + টিএছ + ব্লু-ৰে + ডব + টিসি + ডিভিডি + ৪কে + ইউএচডি + এইচডিৰ + এসডিৰ + ওয়েব + পোষ্টাৰ ছবি + প্লেয়াৰ + দৰ্শনীয়তা আৰু শীৰ্ষক + শীৰ্ষক + দৰ্শনীয়তা + অবৈধ আইডি + পছন্দৰ মিডিয়া ভাষাৰ দ্বাৰা ফিল্টাৰ কৰক + সাবটাইটেলত ব্লোট দূৰ কৰক + অতিৰিক্ত + ট্ৰেলাৰ + ক্ৰেশ ৰিপ\'টিং + https://example.com/example.mp4 + পূৰ্বৱৰ্তী + ছেটআপ প্ৰস্থান কৰক + আপোনাৰ ডিভাইচ অনুযায়ী এপ্পৰ প্ৰস্তুতি সলনি কৰক + আপুনি কি চাব + এক্সটেনছনসমূহ + প্ৰায়ণশাল যোগ কৰক + প্ৰায়ণশালৰ নাম + প্ৰায়ণশাল URL + প্লাগইন লোড হোৱা হৈছে + প্লাগইন ডাউনলোড হোৱা হৈছে + প্লাগইন মুছিলো + %s লোড কৰিব পৰা নহয় + ১৮+ + %1$d %2$s ডাউনলোড আৰম্ভ কৰা হ’ল… + %1$d %2$s ডাউনলোড কৰা হ’ল + সকলো %s ইতিমধ্যে ডাউনলোড কৰা হৈছে + ৰিপ’চ্যুটৰীত কোনো প্লাগইন পোৱা নাই + প্লাগইন + প্লাগইনসমূহ + ৰিপ’চ্যুটৰি পোৱা নাই, URL চেক কৰক আৰু VPN চেক কৰক + এইটো আৰুও ৰিপ’চ্যুটৰীত সকলো প্লাগইন মুছিব + ৰিপ’চ্যুটৰি মুছিব + %d প্লাগইন আপডেট কৰা হ’ল + নিষ্ক্ৰিয় কৰা হ’ল: %d + ডাউনলোড হোৱা নাই: %d + ক্লাউডস্ট্ৰীমত ডিফ’ল্টত কোনো বেছি সাইট ইনষ্টল কৰা নাই। আপোনাৰ এটি পৰিৱেশনত থকা সাইটসমূহ ইনষ্টল কৰিব লাগিব। +\n +\nআমাৰ ডিসক’ৰ্ডত যোগদান কৰক অথবা অনলাইনত সন্ধান কৰক। + সম্প্রদায়ৰ প্ৰায়ণশালসমূহ চাওক + সকলো সাবটাইটেলৰ মাজতে আপাৰ আকাৰত লিখক + এই প্ৰায়ণশালৰ সকলো প্লাগিন ডাউনলোড কৰিব? + %s (অক্ষম কৰা আছে) + পথসমূহ + অডিঅ’ পথসমূহ + ভিডিঅ’ পথসমূহ + প্ৰযোগ সলনি কৰিবলৈ এপ্‌লিকেছন পুনৰ আৰম্ভ কৰক। + পুনৰ আৰম্ভ কৰক + নিৰাপদ প্ৰণামী চালু + এপ্প সমস্থ একটি দ্বিঘাতৰ ফলে সমস্থ এক্সটেনছন নিষ্ক্ৰিয় কৰা হৈছিল যিতে আপোনাক কিবা সমস্যা আছে তা চেনা নিব পাৰে। + দ্বিঘাতৰ তথ্য চাওক + ৰেটিং: %s + বিৱৰণ + সংস্কৰণ + অৱস্থা + আকাৰ + লেখক + সমৰ্থিত + ভাষা + শীৰ্ষকত প্লাগিনটো ইনষ্টল কৰক + এপ্‌লিকেছন পোৱা নহয় + সকলো ভাষা + %s অপচাৰ কৰক + ওপনিং + শেষ + পুনৰাবৃত্তি + HLS প্লেলিষ্ট + পছন্দৰ ভিডিঅ’ প্লেয়াৰ + আইণ্টাৰনেল প্লেয়াৰ + MPV + Web ভিডিঅ’ কাষ্ট + Fcast + Web ব্ৰাউজাৰ + কাষ্ট ডিভাইচ নিৰ্বাচন কৰক + মিছিণ শেষ + মিছিণ আৰম্ভ + ইতিহাস মুকা + শ্ৰেয়া + প্ৰস্তাবনা + ইতিহাস + ডাটাবেছত প্ৰবেশ/শেষৰ বাবে পপাপ দেখাওক + বেছি পাঠ্য। ক্লিপব’ৰ্ডত সংৰক্ষণ কৰিব নোৱা যায়। + কপি কৰিবলৈ ত্ৰুটি, অনুগ্ৰহ কৰি লগকেট কপি কৰি অ্যাপ সমৰ্থনকৰ্তাৰ সৈতে যোগাযোগ কৰক। + চাওক হিচাপে চিহ্নিত কৰক + চাওক হিচাপৰ পৰা মুকা + হয় + নহয় + ঠিক আছে + আপুনি নিশ্চিত হৈছে যে আপুনি প্ৰস্থান কৰিব বিচাৰে? + অ্যাপ্‌ৰ বেটেৰি ব্যৱহাৰ ইতিমধ্যে অসীমিত হ’লে + সাবস্ক্ৰাইব কৰা TV সেৰাৰ বাবে অবিচ্ছিন্ন ডাউনলোড আৰু বিজ্ঞাপনৰ বাবে নোটিফিকেশনৰ বাবে, ক্লাউডষ্ট্ৰিমৰ অনুমতি প্ৰয়াপ্ত কৰিবলৈ পৃষ্ঠভূমিত চলকৰ বাবে অনুমতি প্ৰয়াপ্ত কৰিবলৈ লাগে। ঠিক আছে টিপিবলৈ, আপুনি এপ তথ্যলৈ দীঘল হৈ যাব। তত্ত্বাবধানে, ইয়াৰ ব্যৱহাৰ ক্লাউডষ্ট্ৰিমক আপোনাৰ বেটাৰী সেক্ষাৰ কৰিব নাই অৰু। ইয়া শুধু প্ৰয়োজনীয় হোৱা সময়ত বেক্গ্‌গ্‌ত অতিৰিক্ত কাৰ্য কৰিবলৈ অপাৰে, যেনে নোটিফিকেশন সোধা আৰু আধিকাৰিক প্ৰস্তাবনাৰ ভিডিঅ’স ডাউনলোড কৰিবলৈ। যদি আপুনি বাতিল কৰিব বাচন কৰিছে, আপুনি পৰেন্তু সেটিং ইয়াৰ পিছতে ক্রোয়জবলৈ চাব পাৰে জেনে গওঁক সেটিংছ। + ক্লাউডষ্ট্ৰীমৰ অ্যাপ্‌ তথ্য খোলাত অসমৰ্থ + অ্যাপ্‌ৰ নতুন সংস্কৰণ ইনষ্টল কৰিব পৰা নাই + অ্যাপ্‌ আপডেট ডাউনলোড হৈছে… + অ্যাপ্‌ আপডেট ইনষ্টল হৈছে… + পুৰণৰূপ + পেকেজ ইনষ্টলাৰ + অ্যাপ্‌ প্ৰস্থানত আপডেট হৈ যাব + ক্ৰমৰ অনুযায়ী + সাজোৱা + ৰেটিং (উচ্চ থকাৰ পৰা নিম্ন) + ৰেটিং (নিম্ন থকাৰ পৰা উচ্চ) + আপডেট হোৱা (নতুনৰ পৰা পুৰণৰূপ) + আপডেট হোৱা (পুৰণৰূপৰ পৰা নতুন) + বৰ্ণমালা (এ থকা জে পৰা জে) + গোটি বাছক কৰক + সৈতে খোলক + আপোনাৰ গোটিটো খালি আছে :( +\nগোটিত প্ৰৱেশ কৰিবলৈ এখনকা একাউণ্টত লগিন কৰক নাইবা আপোনাৰ স্থানীয় গোটিত চইক যোগ কৰক। + এই তালিকাটো খালি আছে। আৰু এটা অন্য এটা তালিকাত স্থানান্তৰ কৰিব চেষ্টা কৰক। + নিৰাপত ম’ড ফাইল পোৱা গৈছে! +\nফাইল মুকা হোৱা পৰা প্ৰাৰম্ভত কোনো এক্সটেনচন লোড কৰা হ’ব নহয়। + পুনৰ প্ৰয়াণ কৰক + সাবস্ক্ৰাইব কৰা প্ৰদর্শন আপডেট হৈ আছে + %s সাবস্ক্ৰাইব কৰা হ\'ল + %s পৰা সাবস্ক্ৰাইব কৰা হ\'ল + ওয়াইফাই + মোবাইল ডাটা + ডিফ’ল্ট ঠিকনা কৰক + ব্যৱহাৰ কৰক + সম্পাদনা কৰক + প্ৰ’ফাইলসমূহ + সাহায্য + ইয়াত আপুনি সৃষ্টিগত উৎসসমূহ কেমানে সাজোৱা পৰিবৰ্তন কৰিব পাৰে। যদি এটা ভিডিঅ’ এটা উচ্চ অগ্ৰাধিকাৰ থাকে তেনেহলে ই উচ্চ মানৰ উৎস বাছনি তথ্যত প্ৰদৰ্শিত হ\'ব। উৎসৰ প্ৰাথমিকতা আৰু মানৰ প্ৰাথমিকতাৰ যোগফল ভিডিঅ’ৰ প্ৰাথমিকতা হ’ব। +\n +\nউৎস A: 3 +\nগুণগত মান B: 7 +\n10 এৰি মানৰ এটা সংযুক্ত ভিডিঅ’ প্ৰাথমিকতা থাকিব। +\n +\nনোট: যদি যোগফল 10 বা তাতো অধিক হ\'ব তেনেহলে প্লেয়াৰ আটোমেটিকলি সেই লিংক লোড হোৱা সময়ত লোড কৰিব নোৱা হ\'ব! + প্ৰ’ফাইল পৃষ্ঠভূমি + ইউআই সঠিকভাৱে সৃষ্টি কৰা নাই, এটা মুখ্য বুগ আৰু অধীৰ অনুমতি দিয়া হোৱা উচিত এইটো পৰ্যন্ত প্ৰতিবেদন কৰক %s + আপুনি ইতিমধ্যে ভোট দিয়া আছে + প্ৰিয়মূল্য বিষয়বস্তুসমূহ + %s প্ৰিয়মূল্য বিষয়বস্তুসমূহত যোগ কৰা হ\'ল + %s প্ৰিয়মূল্য বিষয়বস্তুসমূহৰ পৰা আঁতৰাই কৰা হ\'ল + প্ৰিয়মূল্য বিষয়বস্তুসমূহত যোগ কৰক + প্ৰিয়মূল্য বিষয়বস্তুসমূহৰ পৰা আঁতৰাই কৰক + সম্ভাৱ্য ডুপ্লিকেট পোৱা গৈছে + সকলোত প্ৰতিস্থাপন কৰক + আপোনাৰ গোটিত এটা সম্ভাব্য ডুপ্লিকেট আইটেম অলপ অস্তিত্বত আছে: \'%s\' +\n +\nআপুনি ই আইটেমটো সংযোগ কৰিব বিচাৰে নে, বৰং ইতিমধ্যে থকা আইটেমটো সংস্কৰণ কৰিব নে, নাইবা অ্যাকছনটো বাতিল কৰিব? + আপোনাৰ গোটিত এক সম্ভাব্য ডুপ্লিকেট আইটেম অনেক অস্তিত্বত আছে: +\n +\n%s +\n +\nআপুনি ই আইটেমটো সংযোগ কৰিব বিচাৰে নে, বৰং ইতিমধ্যে থকা আইটেমটোসমূহ সংস্কৰণ কৰিব নে, নাইবা অ্যাকছনটো বাতিল কৰিব? + %sৰ বাবে PIN এন্টাৰ কৰক + বৰ্তমান পিন এন্টাৰ কৰক + প্ৰ’ফাইল লক কৰক + পিন + অশুদ্ধ পিন। অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক। + পিনটো 4 বৰ্ণ থাকিব লাগিব + এটা একাউন্ট নিৰ্বাচন কৰক + একাউন্টসমূহ পৰিচালনা কৰক + প্ৰস্থানত একাউন্ট নিৰ্বাচন পাছ দিব + ডিফ’ল্ট একাউন্ট ব্যৱহাৰ কৰক + ঘূৰাওক + %sৰ হিচাপে লগ ইন কৰা হৈছে + স্ক্ৰীনৰ অৰিএণ্টেশ্বনৰ বাবে ট’গল বুটাম প্ৰদৰ্শন কৰক + ভিডিঅ’ৰ অৰিএণ্টেশ্বনৰ ভিত্তিত স্বয়ংক্ৰিয় স্ক্ৰীনৰ অৰিএণ্টেশ্বন প্ৰবণ কৰক + স্বয়ংক্ৰিয় ঘূৰাওক + প্ৰিয়মূল্যহীন + এই ডিভাইচত বায়োমেট্ৰিক পুনৰ্প্ৰমাণৰ সমৰ্থন কৰা নাই + আঙুলি ছাঁচ ব্যৱহাৰ কৰি, মুখৰ চিত্ৰ, PIN, প্ৰণালী আৰু পাছৱাৰ্ডৰ সৈতে অ্যাপ্‌ আনলক কৰক। + প্ৰিয়মূল্য + এই স্ক্ৰীন একাধিক অসফল চেষ্টাৰ কাৰণে বন্ধ হৈছিল। অনুগ্ৰহ কৰি অ্যাপ্লিকেশ্বন পুনৰ্‌ আৰম্ভ কৰক। + আপোনাৰ CloudStream ডাটা এতিয়া বেকআপ কৰা হৈছে। হৈচঁদিক এই সম্ভাবনা বেছি নাই, সকলো ডিভাইচত বিভিন্নভাৱে আচৰণ কৰিব পাৰে। যদিচয় আপুনি এপ্পটো প্ৰৱেশ কৰাৰ বন্ধ হৈ যাওৱাৰ অভাবতে, অ্যাপ্‌ৰ তথ্য পূৰ্ণভাৱে মচলা কৰক আৰু এক বেকআপৰ পৰা প্ৰতিস্থাপন কৰক। এই বিষয়ত উদ্ভাবিত যোৱা অসুবিধাৰ বাবে আমি অত্যন্ত দুঃখী। + ডাউনলোড কৰক + এপ্‌টোক আৰম্ভ কৰাৰ পাছত নতুন উন্নয়নসমূহ স্বয়ংক্ৰিয়ভাৱে খোঁজি পাওক। + পূৰ্ণ মুক্তি প্ৰাপ্ত কৰিবলৈ প্ৰি-ৰিলিজ উন্নয়ন খোঁজি পাওক। + একেই ডেভল\'পাৰকৰ অনিমে এপ্‌ + নতুন আপডেট পাইছো! +\n%1$s -> %2$s + ফিলাৰ + CloudStream ৰ জৰিয়তে খেলোৱা + সন্ধান + ডাউনলোডসমূহ + টেগসমূহ + লোডিং এৰি যাওক + লোড হৈ আছে… + ধৰি ৰখা হৈ আছে + সম্পন্ন হৈ গ\'ল + এৰি দিয়া হৈছে + পুনৰাবৃত্তি কৰা হৈ আছে + চলচ্চিত্ৰ খেলাওক + ট্ৰেলাৰ খেলাওক + লাইভষ্ট্ৰীম খেলাওক + ছাবটাইটেল বাছনি কৰক + পৰ্ব খেলাওক + প্ৰয়োগ কৰক + \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 2be08369..5a104444 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -422,7 +422,7 @@ Вижте хранилищата на общността Публичен списък Всички субтитри с главни букви - Изтегляне на всички добавки от това хранилище\? + Изтегляне на всички добавки от това хранилище? %s (Деактивиран) Потоци Аудио потоци @@ -601,4 +601,4 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. - + \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 867dd4ed..ccd9e433 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -56,7 +56,7 @@ ডাউনলোড শুরু ডাউনলোড বাদ ডাউনলোড শেষ - স্ট্রিম + নেটওয়ার্ক স্ট্রিম লিংক লোডিং ব্যর্থ ডাব সাব @@ -119,7 +119,7 @@ ক্রোমক্যাস্ট এ সাবটাইটেল সমূহের সেটিংস কালো প্রান্ত অপসারণ করুন অনুসন্ধান করুন - অ্যাকাউন্টসমূহ + অ্যাকাউন্টসমূহ এবং নিরাপত্তা কোনো উপাত্ত পাঠাবে না বিরতি দিতে মাঝে দুইবার চাপুন সিস্টেম এর উজ্জ্বলতা ব্যবহার করুন @@ -143,7 +143,7 @@ পোস্টার @string/home_play আগাতে ডবল ট্যাপ করুন - আইজেনগ্রাভি মোড + প্লেব্যাক এর গতি আপডেট শুরু হয়েছে ব্রাউজার লগ @@ -229,4 +229,134 @@ আপনার বর্তমান পর্বের অগ্রগতি স্বয়ংক্রিয়ভাবে সিঙ্ক করুন প্লাগইন ডাউনলোড ফিল্টার করতে মোড নির্বাচন করুন লিঙ্ক পুনরায় লোড হয়েছে - + সুইচ অ্যাকাউন্ট + ব্রাউজারে প্লে করুন + দাবিত্যাগ + এশিয়ান ড্রামা + সোর্স + এক্সটেনশন + লিংকস + এনএসএফডব্লিউ + ডাউনলোড মিরর + অপ্রত্যাশিত প্লেয়ার এর সমস্যা + সাবটাইটেল ডাউনলোড করুন + রিপোজিটরির নাম এবং ইউ আর এল + কপি করা হয়েছে! + অ্যান্ড্রয়েড টিভির মতো, কম মেমরির ডিভাইসে খুব বেশি সেট করা হলে সমস্যা করবে। + ক্লোন সাইট + প্লেয়ারের ফিচার + MAL AniList TMDB IMDB Kitsu Trakt %1$s%2$s + অ্যাপ থিম + রিকমেন্ডেশনগুলো দেখাও + প্লেয়ারে গতির বিকল্প যোগ কর + %1$s %2$d%3$s + কার্টুন + এনিমে + পোস্টারে ইউ আই উপাদান টগল করুন + আপডেট চেক করুন + রিসাইজ + ওপেনিং স্কিপ করুন + ডিক্সের ভিডিও ক্যাশ + ভিডিও এবং ইমেজ ক্যাশ পরিস্কার করুন + অ্যান্ড্রয়েড টিভির মতো, কম মেমরির ডিভাইসে খুব বেশি সেট করা হলে ক্র্যাশ করবে + ডিএনএস ওভার এইচটিটিপিএস + একটি ভিন্ন URL সহ একটি বিদ্যমান সাইটের, একটি ক্লোন যোগ করুন + স্ক্রিনে ফিট করুন + ক্যাশ + লেআউট + টিভি লেআউট + ব্যবহারকারীর নাম + %1$d-%2$d + NewSiteName + ডিফল্ট + OVA + ওভিয়ে + টরেন্ট + এপিসোড ক্রোমকাস্ট করুন + প্লে হচ্ছে %s সময়ের মধ্যে + লিঙ্ক কপি করুন + স্বয়ংক্রিয় ডাউনলোড + টাইটেল + প্লেয়ার দেখা যাচ্ছে - সিকের পরিমাণ + রিমুভ সাইট + NGINX সার্ভারের ইউআরএল + আইএসপি বাইপাস + অ্যান্ড্রয়েড টিভি + সমর্থিত এক্সটেনশনগুলিতে NSFW সক্ষম করুন + সাবটাইটেল এনকোডিং + দেখার ধরন + ফিচার সমূহ + হোমপেজ এবং লাইব্রেরিতে এলোমেলো বোতাম দেখান + প্রদানকারী + প্রদানকারী পরীক্ষা + স্বয়ংক্রিয় + ফোন লেআউট + পোস্টার শিরোনামের অবস্থান + পোস্টারের নীচে শিরোনাম রাখুন + প্রবেশ করুন + অ্যাকাউন্ট তৈরি করা + একাউন্ট যোগ করা + যখন প্লেয়ার হিডেন থাকবে তখন সিকের পরিমান + ক্রোমকাস্ট মিরর + এক্সটেনশন ভাষা + লেআউট + সাবটাইটেল + অ্যাকশন + ভিডিও প্লেয়ারের টাইটেল এ সর্বোচ্চ ক্যারেক্টার + ডকুমেন্টারি + অ্যাপ লেআউট + ভিডিও + বেকাপ + E + S + hello@world.com + https://example.com + নতুন এপিসোডের নোটিফিকেশন + অন্য এক্সটেনশনের মধ্যে খুঁজুন + কোন আপডেট পাওয়া যায়নি + password123 + আসছে %s সময়ের মধ্যে + বাতিল করুন + %s +\nঅবশিষ্ট + লাইভ স্ট্রিম + সোর্স সমস্যা + রিমোট সমস্যা + রেন্ডারের সমস্যা + ডাউনলোডের সমস্যা, স্ট্রোরেজদের পারমিশন চেক করুন + অ্যাপ এ প্লে করুন + লিংক রিলোড করুন + কোয়ালিটি লেবেল + সাব লেবেল + ডাব লেবেল + লক + আর দেখাবেন না + এই আপডেট স্কিপ করুন + আপডেট + ওয়াইফাই তে যে কোয়ালিটিতে দেখতে চান + মোবাইল ডাটায় যে কোয়ালিটিতে দেখতে চান + ভিডিও প্লেয়ারে রেজুলেশন + ভিডিও বাফার সাইজ + ভিডিও বাফার লেনথ + যখন আইএসপি ব্লক করবে তখন কার্যকরী + গিডহাব প্রক্সি + ডাউনলোডের পথ + ডাব/সাব এনিমে দেখান + স্ট্রেচ + বড় করুন + অ্যাপ আপডেট + গ্যাসচার + ডিফল্ট + সাধারণ + এলোমেলো বোতাম + পছন্দের মিডিয়া + সমস্ত এক্সটেনশন পরীক্ষা করুন + এই পরীক্ষাটি শুধুমাত্র ডেভেলপারদের জন্য এবং কোন এক্সটেনশনের কাজ যাচাই বা অস্বীকার করে না। + এমুলেটর লেআউট + প্রাইমারি রং + 127.0.0.1 + Language code (en) + অ্যাকাউন্ট + প্রস্থান + %1$d%2$s + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 40847edf..3042fa21 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -422,7 +422,7 @@ Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas - Transferir todos os plugins deste repositório\? + Atenção: CloudStream 3 não assume nenhuma responsabilidade pelo uso de extensões de terceiros e não fornece nenhum suporte para elas! %s (Desativado) Reproduzir automaticamente próximo episódio Começa o próximo episódio quando o atual termina @@ -622,7 +622,7 @@ Autenticação de Senha/PIN A autenticação biométrica não é compatível com este dispositivo Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha. - Esta tela foi fechada devido a diversas tentativas malsucedidas. Por favor reinicie o aplicativo. + Após algumas tentativas fracassadas, o prompt será fechado. Basta reiniciar o aplicativo para tentar novamente. %s \nrestante(s) Favorito @@ -639,4 +639,21 @@ Música Áudio-livro Mídia - + Redefinir + Próximos em %s + Temporada %1$d Episódio %2$d será lançado em + Fcast + Selecione o dispositivo de transmissão + Espelhar transmissão + CloudStream Wiki + Segurança + Contas + Autenticação local + Imagem do código QR + Descartar + Abrir repositório + Acesse %s em seu smartphone ou computador e digite o código acima + Não é possível obter o código PIN do dispositivo, tente a autenticação local + O código PIN expirou! + O código expira em %1$dm %2$ds + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0a8cf997..e1c51874 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -489,7 +489,7 @@ Přidat klon existujícího webu s jinou adresou URL https://example.com Kód jazyka (cs) - Stáhnout všechny doplňky z tohoto repozitáře\? + Varování: CloudStream 3 nenese žádnou zodpovědnost za používání rozšíření třetích stran a neposkytuje pro ně žádnou podporu! %s (zakázáno) Stopy NSFW @@ -623,7 +623,7 @@ Ověření heslem/PINem Biometrické ověření není na tomto zařízení podporováno Odemkněte aplikaci otiskem prstu, obličejem, PINem, gestem nebo heslem. - Tato obrazovka byla po několika nezdařilých pokusech uzavřena. Restartujte prosím aplikaci. + Po několika nezdařilých pokusech se okno zavře. Pro opětovný pokus restartujte aplikaci. Vaše data z aplikace CloudStream byla nyní zálohována. Ačkoli je tato možnost velmi malá, různá zařízení se mohou chovat různě. Ve výjimečném případě, že se vám přístup k aplikaci zablokuje, data aplikace zcela vymažte a obnovte je ze zálohy. Velmi se omlouváme za případné nepříjemnosti z toho plynoucí. Odebrat z oblíbených %s @@ -641,4 +641,21 @@ Zakažte optimalizace baterie Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. Audiokniha - + Resetovat + Vychází %s + Epizoda %2$d ze série %1$d bude vydána za + Vysílat zrcadlení + Fcast + Vyberte zařízení k vysílání + CloudStream Wiki + Zabezpečení + Obrázek QR kódu + Zavřít + Otevřít repozitář + Navštivte %s na vašem zařízení nebo počítači a zadejte kód výše + Nepodařilo se získat PIN kód, zkuste místní ověření + Kód vyprší za %1$dm %2$ds + Účty + Lokální ověření + PIN kód vypršel! + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5a871217..d111ed68 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -151,7 +151,7 @@ Speicherberechtigungen fehlen. Bitte erneut versuchen. Suche Konten und Sicherheit - Updates und Datensicherung + Aktualisierungen und Datensicherung Info Erweiterte Suche Liefert die Suchergebnisse getrennt nach Anbietern @@ -416,12 +416,12 @@ Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben - Alle Plugins aus diesem Repository herunterladen\? + Alle Plugins aus diesem Repository herunterladen? %s (Deaktiviert) Spuren Audiospuren Videospuren - Bei Neustart anwenden + Starte die App neu, um die Änderungen zu sehen. Abgesicherter Modus aktiviert Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, welche Probleme verursacht. Absturzinfo ansehen @@ -607,4 +607,12 @@ Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. Fehler beim zugriff auf die Zwischenablage, bitte erneut versuchen. Repository Name und URL - + OK + Akku-Optimierung deaktivieren + Musik + Hörbuch + Medien + Zurücksetzen + Akkuverbrauch der App ist bereits auf unbeschränkt eingestellt + CloudStreams App-Info kann nicht geöffnet werden. + \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a539f374..dbf03fb8 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Ρυθμίσεις υποτίτλων του προγράμματος αναπαραγωγής Υπότιτλοι για Chromecast Ρυθμίσεις υποτίτλων για Chromecast - Eigengravy Mode + Ταχύτητα Προβολής Σύρετε για αναζήτηση Σύρετε από πλευρά σε πλευρά για να ελέγξετε το σημείο του βίντεο στο οποίο βρίσκεστε Σύρετε για να αλλάξετε ρυθμίσεις @@ -130,7 +130,7 @@ Τα δεδομένα αποθηκεύτηκαν Δεν έχει δοθεί άδεια για πρόσβαση στον αποθηκευτικό χώρο. Παρακαλώ προσπαθήστε ξανά. Σφάλμα δημιουργίας αντιγράφων ασφαλείας %s - Λογαριασμοί + Λογαριασμοί και Ασφάλεια Ενημερώσεις και αντίγραφα ασφαλείας Εμφάνιση filler επεισοδίου για άνιμε Εμφάνιση trailer @@ -201,7 +201,7 @@ Επαναφόρτωση συνδέσμων Λήψη υποτίτλων Ποιότητα - Dub + Ετικέτα Dub Sub Τίτλος Εναλλαγή γραφικών στοιχείων στην αφίσα @@ -233,11 +233,11 @@ Αποποίηση ευθυνών Γενικά Κουμπί τυχαίας δράσης - Εμφάνιση κουμπιού τυχαίας δράσης στην Αρχική Οθόνη + Εμφάνιση κουμπιού τυχαίας προβολής στην Αρχική Οθόνη Γλώσσες παρόχων Διάταξη εφαρμογής Προτιμώμενα μέσα - Ενεργοποίηση NSFW σε υποστηριζόμενους παρόχους + Ενεργοποίηση ακατάλληλου περιεχομένου σε υποστηριζόμενους παρόχους Κωδικοποίηση υποτίτλων Πάροχοι Διάταξη @@ -302,8 +302,8 @@ Φιλτράρισμα ανά την προτεινόμενη γλώσσα του μέσου Έξτρα Τρέιλερ - Σύνδεσμος για stream - Παραπομπή + Https://Παράδειγμα.com/Παράδειγμα.mp4 + Παραπομπή (προαιρετική) Επόμενο Παρακολούθηση βίντεο σε αυτή την γλώσσα Προηγούμενο @@ -334,8 +334,6 @@ Ενημερώθηκαν %d πρόσθετα Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. \n -\nΛόγω ενός χαζού DMCA takedown από μέρους των Sky UK Limited 🤮 δεν μπορούμε να προσθέσουμε απευθείας σύνδεσμο προς τα προαναφερόμενα αποθετήρια εντός της εφαρμογής. -\n \nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. Προβολή αποθετηρίων κοινότητας Δημόσια λίστα @@ -345,7 +343,7 @@ Κομμάτια Ηχητικά κομμάτια Κομμάτια βίντεο - Εφαρμογή στην επανεκκίνηση + Κάντε επανεκκίνηση της εφαρμογής για να δείτε τις αλλαγές. Η ασφαλής λειτουργία ενεργοποιήθηκε Όλα τα extensions απενεργοποιήθηκαν , ώστε να μπορέσετε να διαπιστώσετε ποιο από αυτά προκάλεσε τη κατάρρευση. Προβολή πληροφορίας κατάρρευσης @@ -392,7 +390,7 @@ Αυτόματη ενημέρωση plugin Αυτόματη λήψη plugin DNS μέσω HTTPS - παράδειγμα.com + https://παράδειγμα.com HQ TS TC @@ -412,7 +410,7 @@ NSFW Chromecast mirror Σύνδεσμος NGINX σέρβερ - ΟΚουλΙστότοποςΜου + ΝεοΟνομαΙστοτοπου /\?\? /%d %d / 10 @@ -434,7 +432,7 @@ Δεν βρέθηκε ενημέρωση Έλεγχος για ενημέρωση κωδικός123 - ΤοΚουλΨευδώνυμοΜου + ΤοΚουλΟνομαΜου γειασου@κόσμε.com Η γρήγορη, καφέ αλεπού πηδάει πάνω από τον τεμπέλη σκύλο / The quick brown fox jumps over the lazy dog Cam @@ -521,7 +519,7 @@ \nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! Δοκιμή παρόχου Προτιμώμενη ποιότητας παρακολούθησης (Δεδομένα τηλεφώνου) - Διακομιστής μεσολάβησης raw.githubusercontent.com + Διακομιστής μεσολάβησης GitHub Android TV Ενημέρωση εγγεγραμμένων εκπομπών Έγινε εγγραφή σε %s @@ -546,4 +544,85 @@ Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη Απενεργοποιημένο Τέλος - + Συχνότητα δημιουργίας αντιγράφων ασφαλείας + Οι σύνδεσμοι επαναφορτώθηκαν + αντιγράφηκε! + Αναζήτηση σε άλλες επεκτάσεις + Ειδοποίηση για νέο επεισόδιο + Εισαγωγή Κωδικού + Όνομα \"αποθήκης\" και λινκ + Εμφάνιση προτάσεων + Προσθήκη στα Αγαπημένα + Εγγραφή + Αντικατάσταση Όλων + Χρησιμοποίηση Βασικού λογαριασμού + Αφαίρεση από τα Αγαπημένα + Κρυμμένο Πλέιερ - Δευτερόλεπτα Σκιπ + Δευτερόλεπτα Σκιπ όταν ο αναπαραγωγέας είναι κρυφός + Εντάξει + Απενεργοποιήση της εξοικονόμησης της μπαταρίας + Έχετε ήδη ψηφίσει + Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' +\n +\nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; + Εισαγωγή Τρέχον Κωδικού + Κλείδωμα Προφίλ + Ξεκλείδωμα Cloudstream + ΚωδικόςPIN Αυθεντικότητας + Προσθέτει επιλογή ταχύτητας στον αναπαραγωγέα + Σύνδεση ως %s + Περιστροφή + Απεγγραφή + Διαχείριση λογαριασμών + Ενεργοποίηση αυτόματης περιστροφής οθόνης αναλόγως του βίντεο + Αυτόματη περιστροφή + Σεζόν %1$dΕπεισόδιο%2$d θα κυκλοφορήσει + Δευτερόλεπτα Σκιπ όταν φαίνεται ο αναπαραγωγέας (πλειερ) + Δοκιμή όλων των παροχών + Αυτό το τεστ προορίζεται μόνο για τους προγραμματιστές και δε επαληθείει ούτε απορρίπτει την λειτουργία οποιουδήποτε παρόχου. + Fcast + Επιλογή συσκευής για αναμετάδοση + Πρόβλημα στην πρόσβαση στο Clipboard, Παρακαλώ προσπαθήστε ξανά. + Πρόβλημα στην αντιγραφή , Παρακαλούμε αντιγράψτε το logcat και επικοινωνήστε με την υποστήριξη. + Η χρήση μπαταρίας έχει ήδη τεθεί χωρίς περιορισμό + Αδύνατο άνοιγμα των στοιχείων της εφαρμογής Cloudstream. + Αγαπημένα + %s προστέθηκε στα αγαπημένα + %s αφαιρέθηκε από τα αγαπημένα + Πιθανό αντίγραφο βρέθηκε + Προσθήκη + Αντικατάσταση + Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: +\n +\n%s +\n +\nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? + Εισαγωγή Κωδικού για %s + Κωδικός + Εσφαλμένος Κωδικός. Προσπαθήστε ξανά. + Ο κωδικός να περιέχει 4 χαρακτήρες + Επιλογή λογαριασμού + Αφαίρεση από αγαπημένα + Κλείδωμα με βιομετρικά + Έρχεται σε %s + Εμφάνιση Πλέιερ - Δευτερόλεπτα Σκιπ + Παράκαμψη απαγόρευσης από raw github URLs χρησιμοποιώντας jsDelivr. Μπορεί να καθυστερήσει τις ενημερώσεις για μερικές μέρες. + Εμφάνιση κουμπιού για περιστροφή οθόνης + Αγαπημένο + %s +\nαπομένουν + Βιομετρική αυθεντικοποίηση δεν υποστηρίζεται από τη συσκευή + Καστ ταινίας + Για να σιγουρέψουμε πως οι λήψεις ταινιών και οι ειδοποιήσεις για σειρές στο Cloudstream δεν έχουν πρόβλημα, το Cloudstream χρειάζεται άδεια να τρέχει στο παρασκήνιο. Πατώντας ΟΚ θα μεταφερθείτε στις λεπτομέρειες εφαρμογής, από κει πηγαίνετε στην Χρήση μπαταρίας από εφαρμογές και θέσετε την χρήση σε μη περιορισμένη. Να έχετε στο νου σας πως το Cloudstream δε θα καταναλώσει την μπαταρία σας. Απλά θα λειτουργήσει μόνο όταν χρειάζεται, όπως για την ειδοποίηση για ανερχόμενες σειρές ή της λήψεις σας μέσω των παροχών. Άμα θέλετε να ακυρώσετε, μπορείτε να αλλάξετε αυτή τη ρύθμιση μέσω των γενικών ρυθμίσεων. + Ξεκλείδωμα εφαρμογής με δακτυλικό αποτύπωμα, Face ID, PIN, Μοτίβο και Κωδικό. + Η οθόνη έκλεισε λόγω πολλαπλών ανεπιτυχών ενεργειών. Κάντε επανεκκίνηση της εφαρμογής. + Επεξεργασία λογαριασμού + Παράλειψη επιλογής λογαριασμού στην εκκίνηση της εφαρμογής + Μουσική + Ακουστικό Βιβλίο + Μέσα + Επαναφορά + Τα δεδομένα σας στο CloudStream έχουν κάνει back up. Αν και η πιθανότητα είναι πολύ χαμηλή, όλες οι συσκευές συμπεριφέρονται διαφορετικά. Στη σπάνια περίπτωση, που απαγορευτεί η πρόσβασή σας από την εφαρμογή, διαγράψτε τα δεδομένα εφαρμογής και επαναφέρετέ τα από ένα ήδη υπάρχον backup. Συγνώμη για οποιαδήποτε ταλαιπωρία. + Λογαριασμοί + Ασφάλεια + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 20484cd9..011762ba 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -176,7 +176,7 @@ Tipo de Borde Elevación de Subtítulo Buscar usando proveedores - Continúa la reproducción en un reproductor miniatura encima de otras aplicaciones + Continúa la reproducción en una imagen pequeña encima de otras aplicaciones Botón de cambio de tamaño del reproductor Eliminar bordes negros Seleccionar idioma automáticamente @@ -232,7 +232,7 @@ Mostrar los resultados de la búsqueda por proveedor Solo envíar los datos si la App se cierra / falla inesperadamente No enviar datos - Mostrar los trailers + Mostrar avances Mostrar pósters de Kitsu Actualizar a las versiones preliminares Buscar actualizaciones preliminares (beta) en lugar de solo versiones completas (stable releases) @@ -289,7 +289,7 @@ CloudStream no tiene sitios instalados por defecto. Necesitas instalar los sitios desde los repositorios. \n \nÚnase a nuestro Discord o busque en línea. - ¿Descargar todos los plugins de este repositorio? + Advertencia: ¡CloudStream 3 no asume ninguna responsabilidad por el uso de extensiones de terceros y no brinda ningún soporte para ellas! Mostrar actualizaciones de la aplicación Instalador de APK Algunos dispositivos no soportan el nuevo instalador de paquetes. Pruebe la opción antigua (legacy) si las actualizaciones no se instalan. @@ -339,7 +339,7 @@ Alternar elementos de la interfaz de usuario en el póster No se encontró ninguna actualización General - Color primario + Color principal Tema de la aplicación hola@mundo.com %1$s %2$s @@ -449,13 +449,13 @@ Descarga por lotes plugin plugins - Actualizados %d plugins + %d plugins actualizados Ver repositorios de la comunidad Lista pública Pistas Pistas de audio Pistas de video - Modo seguro ON + Modo seguro activado Ver información de fallos Puntaje:%s Versión @@ -483,7 +483,7 @@ Si La aplicación se actualizará al salir Actualización iniciada - Complemento descargado + Plugin descargado Quitar de visto Ordenar por Ordenar @@ -512,7 +512,7 @@ Registro Empezar Aprobado - Prueba del proveedor + Verificar al proveedor Reiniciar Suscrito Suscrito a %s @@ -545,7 +545,7 @@ La interfaz de usuario no se ha podido crear correctamente, se trata de un GRAN BUG y debe ser reportado inmediatamente %s Seleccionar modo para filtrar los plugins descargados Deshabilitar - No se encontraron complementos en el repositorio + No se encontraron plugins en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN Ya has votado Frecuencia de la copia de seguridad @@ -599,7 +599,7 @@ Desbloquea la aplicación con huella dactilar, Face ID, PIN, patrón y contraseña. Desbloquear CloudStream La autenticación biométrica no es compatible con este dispositivo - Esta pantalla se cerró después de algunos intentos fallidos. Reinicie la aplicación. + Después de algunos intentos fallidos, el mensaje se cerrará. Simplemente reinicie la aplicación para volver a intentarlo. Ahora se ha realizado una copia de seguridad de sus datos de CloudStream. Aunque la posibilidad de que esto ocurra es muy baja, todos los dispositivos pueden comportarse de forma diferente. En el raro caso de que no puedas acceder a la aplicación, borra completamente los datos de la aplicación y restaura desde una copia de seguridad. Sentimos mucho las molestias que esto pueda ocasionarte. Favorito No favorito @@ -616,5 +616,22 @@ No se puede abrir la información de la aplicación CloudStream. Media Audiolibro - Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. - + Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta 𝙐𝙨𝙤 𝙙𝙚 𝙡𝙖 𝙗𝙖𝙩𝙚𝙧í𝙖 𝙙𝙚 𝙡𝙖 𝙖𝙥𝙡𝙞𝙘𝙖𝙘𝙞ó𝙣 y establezca el uso de la batería en 𝙎𝙞𝙣 𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙘𝙞𝙤𝙣𝙚𝙨. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en 𝙡𝙤𝙨 𝙖𝙟𝙪𝙨𝙩𝙚𝙨 𝙜𝙚𝙣𝙚𝙧𝙖𝙡𝙚𝙨. + Reset + Próximamente en %s + La temporada %1$d y el episodio %2$d se estrenarán en + Seleccionar el dispositivo para transmitir + Fcast + Espejo de transmisión + Wiki de CloudStream + Seguridad + Cuentas + Autenticación local + Imagen del código QR + Descartar + Repositorio abierto + Visita %s en tu smartphone o ordenador e introduce el código anterior + ¡El código PIN ya ha caducado! + El código caduca en %1$d mín y %2$d s + No puedo obtener el código PIN del dispositivo; intente con la autenticación local + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 77c3db15..91d23b61 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -280,7 +280,7 @@ Erreur de sauvegarde %s Recherche Comptes et Sécurité - Mises à jour et sauvegarde + Mises à jour et Sauvegarde Info Recherche avancée Vous donne les résultats de la recherche séparés par fournisseur @@ -419,7 +419,7 @@ Télécharger la liste de sites que vous voulez utiliser Téléchargé : %d Pistes vidéo - Appliqué au redémarrage + Redémarrez l\'application pour voir les changements. Toutes les extensions ont été désactivé à cause d\'un crash pour vous aider à trouver l\'extension causant le problème. Mode sans échec activé Taille @@ -446,7 +446,7 @@ Désactivé : %d Non téléchargé : %d %d plugins mis-à-jour - Télécharger tous les plugins de ce repository \? + Télécharger tous les plugins de ce repository ? %s (Désactivé) Pistes Pistes audio @@ -595,4 +595,29 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - + Favori + Vos données CloudStream viennent d\'être sauvegardées. Bien que cette éventualité soit très faible, tous les appareils peuvent se comporter différemment. Dans le rare cas où l\'accès à l\'application est bloqué, effacez complètement les données de l\'application et restaurez à partir d\'une sauvegarde. Nous sommes sincèrement désolés pour les désagréments occasionnés par cette situation. + Désactiver l\'optimisation de la batterie + Impossible d\'ouvrir les informations de l\'application CloudStream. + Déverrouiller CloudStream + Musique + %s +\nrestants + Erreur d\'accès au presse-papiers, veuillez réessayer. + OK + L\'authentification biométrique n\'est pas prise en charge sur cet appareil + Livre Audio + Mot de passe/Code PIN + Erreur de copie, Veuillez copier le logcat et contacter le support de l\'application. + Déverrouiller l\'appli avec l\'empreinte digitale, l\'identification faciale, le code PIN, le motif et le mot de passe. + Cet écran a été fermé en raison de plusieurs tentatives infructueuses. Veuillez relancer l\'application. + Pour garantir des téléchargements ininterrompus et des notifications pour les émissions de télévision auxquelles vous êtes abonné, CloudStream a besoin d\'une autorisation pour fonctionner en arrière-plan. En appuyant sur OK, vous serez dirigé vers App info. Faites défiler jusqu\'à 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 et réglez l\'utilisation de la batterie sur 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Veuillez noter que cette autorisation ne signifie pas que CS3 épuisera votre batterie. Il ne fonctionnera en arrière-plan que lorsque cela sera nécessaire, par exemple lors de la réception de notifications ou du téléchargement de vidéos à partir d\'extensions officielles. Si vous choisissez d\'annuler, vous pouvez ajuster ce paramètre ultérieurement dans 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + L\'utilisation de la batterie de l\'application est déjà réglée sur illimitée + Supprimer des favoris + Média + Réinitialiser + À venir dans %s + Verrouillage biométrique + Sélectionnez un appareil de diffusion + Saison %1$d Episode %2$d sera publié dans + \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 8ce224b3..b16292ba 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -192,4 +192,21 @@ लिंक पुन्ह खुली वर्तमान पिन दर्ज करें नेटवर्क स्ट्रीम - + साफ़ करें + उपशीर्षक सेटिंग्स + अक्षर का माप + बंद करें + रिपॉजिटरी का नाम और यूआरएल + कॉपी! + सहेजें + नये एपिसोड की अधिसूचना + अन्य एक्सटेंशन में खोजें + सुझाव दिखाएं + पृष्ठभूमि का रंग + रूपरेखा प्रकार + अक्षर का रंग + बॉक्स का रंग + रूपरेखा रंग + उपशीर्षक ऊंचाई + अक्षर शैली + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ea6a80eb..6e35506d 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -11,41 +11,41 @@ %d %.1f/10.0 %d - %1$s Ep %2$d - Cast: %s - Epizoda %d će izaći - %1$dd %2$dh %3$dm - %1$dh %2$dm - %dm + %1$s epizoda %2$d + Glumačka postava: %s + Epizoda %d će izaći za + %1$dd %2$dh %3$dmin + %1$dh %2$dmin + %dmin Poster Poster - Episode Poster - Main Poster - Next Random - Go back - Change Provider - Preview Background + Poster epizode + Glavni poster + Sljedeće slučajno odabrano + Idi natrag + Promijeni pružatelja usluge + Pregled slike pozadine - Brzina (%.2fx) + Brzina (%.2f×) Ocjena: %.1f - Pronađeno novo ažuriranje! + Pronađeno je novo ažuriranje! \n%1$s -> %2$s Umetak %d min CloudStream - Reproduciraj s CloudStream-om + Reproduciraj s CloudStreamom Početna stranica - Pretraži + Traži Preuzimanja Postavke - Pretraži… - Pretraži %s… + Traži … + Traži %s … Nema podataka - Više postavki + Više opcija Sljedeća epizoda Žanrovi - Podijeli + Dijeli Otvori u pregledniku Preskoči učitavanje Učitavanje … @@ -53,35 +53,35 @@ Na čekanju Dovršeno Ispušteno - Planiram pogledati - Ponovno gledam - Pokreni Film + Planiram gledati + Ponovo gledam + Pokreni film Pokreni LiveStream Pokreni Torrent Izvori Titlovi - Ponovno pokušaj povezivanje… + Ponovni pokušaj povezivanja … Idi natrag - Pokreni Epizodu + Pokreni epizodu Preuzmi Preuzeto - Trenutno preuzimam + Preuzimanje u tijeku Preuzimanje pauzirano Preuzimanje započeto - Preuzimanje nije uspjelo + Preuzimanje neuspjelo Preuzimanje otkazano Preuzimanje dovršeno Mrežni stream - Pogreška pri učitavanju veza - Unutarnja pohrana - Dub - Sub + Pogreška pri učitavanju poveznica + Interna pohrana + Sinkronizacija + Titlovi Izbriši datoteku Otvori datoteku Nastavi preuzimanje Pauziraj preuzimanje - Onemogući automatsko izvješćivanje o bugovima + Onemogući automatsko izvješćivanje o greškama Više informacija Sakrij Pokreni @@ -93,99 +93,99 @@ Primijeni Kopiraj Zatvori - Očisti + Izbriši Spremi Brzina playera Postavke titlova Boja teksta - Boja obruba - Pozadinska boja + Boja konture + Boja pozadine Boja prozora - Tip ruba + Vrsta ruba Visina titlova Font Veličina fonta - Pretraži s uslugama - Pretraži s tipovima - %d banana dano developerima + Traži koristeći pružatelje usluga + Traži koristeći vrste + %d banana dano programerima Nisi dao ni jednu bananu Automatski odabir jezika Preuzmi jezike Jezik titlova - Držite za vraćanje na zadane postavke - Uvezi fontove tako da ih postavite u %s - Nastavite s gledanjem + Pritisni za vraćanje na zadane postavke + Uvezi fontove postavljanjem u %s + Nastavi gledati Ukloni Više informacija @string/home_play - Za ispravan rad ovog pružatelja usluga može biti potreban VPN + Za ispravan rad ovog pružatelja usluga je možda potreban VPN Ovaj pružatelj usluga je torrent, preporučuje se VPN - Stranica ne daje metapodatke, učitavanje videozapisa neće uspjeti ako ne postoji na stranici. + Stranica ne sadrži metapodatke. Učitavanje videa neće uspjeti ako ne postoje na stranici. Opis - Plot nije pronađen + Radnja nije pronađena Opis nije pronađen - Prikaži LogMačku 🐈 - Picture-in-picture - Nastavlja reprodukciju u minijaturnom playeru povrh drugih aplikacija - Gumb za promjenu veličine playera - Uklaja crne rubove + Prikaži Logcat 🐈 + Slika u slici + Nastavlja reprodukciju u minijaturnom playeru ispred drugih aplikacija + Gumb za mijenjenje veličine playera + Ukloni crne rubove Titlovi Postavke titlova playera - Chromecast Titlovi + Chromecast titlovi Postavke Chromecast titlova Brzina reprodukcije - Prijeđi prstom za traženje - Prijeđite prstom ulijevo ili udesno kako biste kontrolirali player - Klizni za promjenu postavki - Kliznite prstom ulijevo ili udesno za promjenu svjetline ili glasnoće - Automatski započni sljedeću epizodu - Započne sljedeću epizodu kad trenutna završi - Dodirni dvaput za traženje + Klizni prstom za pomicanje + Klizni prstom ulijevo ili udesno za postavljanje pozicije videa + Klizni prstom za mijenjanje postavki + Klizni prstom prema gore ili dolje na lijevoj ili desnoj strani za mijenjanje svjetline ili glasnoće + Automatski pokreni sljedeću epizodu + Pokreni sljedeću epizodu kada trenutačna epizoda završi + Dodirni dvaput za pomicanje Dodirni dvaput za pauziranje - Iznos preskakanja u playeru (Sekunde) - Dvaput dodirni desnu ili lijevu stranu ekrana za pomicanje naprijed ili natrag - Dodirnite dvaput u sredinu zaslona za pauziranje - Koristi svijetlinu u sustavu + Količina pomicanja u playeru (sekunde) + Dodirni dvaput desnu ili lijevu stranu za pomicanje prema naprijed ili natrag + Dodirni dvaput u sredinu za pauziranje + Koristi svijetlinu sustava Koristi svjetlinu sustava u playeru aplikacija umjesto tamnog preklopa Ažuriraj napredak gledanja Automatski sinkronizira vaš trenutni napredak u filmu ili epizodi - Vraćanje podataka iz sigurnosne kopije + Obnovi podatke iz sigurnosne kopije Sigurnosno kopiranje podataka - Učitana datoteka sigurnosne kopije - Vraćanje podataka iz datoteke nije uspjelo %s + Datoteka sigurnosne kopije je učitana + Obnavljanje podataka iz datoteke %s nije uspjelo Podaci pohranjeni Nedostaju dozvole za pohranu, pokušaj ponovo. Pogreška pri sigurnosnom kopiranju %s - Pretraži + Traži Računi i sigurnost - Ažuriranja i sigurnosne kopije + Ažuriranja i sigurnosna kopija Informacije - Napredno pretraživanje - Daje rezultate pretraživanja odvojene prema pružatelju usluga + Napredna pretraga + Daje rezultate pretrage odvojene prema pružatelju usluga Šalje samo podatke o padovima aplikacije Ne šalje podatke Prikaži dodatnu epizodu za anime Prikaži trailere Prikaži postere iz Kitsua - Sakrij odabranu kvalitetu videozapisa u rezultatima pretraživanja + Sakrij odabranu kvalitetu videa u rezultatima pretrage Automatsko ažuriranje dodataka Prikaži ažuriranja aplikacije Automatski traži nova ažuriranja nakon pokretanja aplikacije. Ažuriranje na predizdanja - Tražite ažuriranja prije izdanja umjesto samo potpunih izdanja + Tražite ažuriranja predizdanja umjesto samo potpunih izdanja Github - Aplikacija za romane od istih developera - Anime aplikacija od istih developera - Uđi u naš Discord - Daj bananu developerima - Dana banana + Aplikacija za romane od istih programera + Anime aplikacija od istih programera + Pridruži se Discordu + Daj bananu programerima + Dane banane Jezik aplikacije Ovaj pružatelj usluga nema podršku za Chromecast - Nisu pronađene veze - Veza je kopirana u međuspremnik + Nisu pronađene poveznice + Poveznica je kopirana u međuspremnik Pokreni epizodu Vrati na zadanu vrijednost - Nažalost, aplikacija se srušila. Anonimno izvješće o bugu bit će poslano developerima + Nažalost se aplikacija srušila. Anonimno izvješće o grešci će se poslati programerima Sezona Nema sezone Epizoda @@ -197,14 +197,14 @@ Nisu pronađene epizode Izbriši datoteku Izbriši - Poništi + Odustani Pauziraj Nastavi - -30 + −30 +30 Ovo će trajno izbrisati %s \nJeste li sigurni\? - %dm + %dmin \npreostalo U tijeku Završeno @@ -244,11 +244,11 @@ Livestream NSFW Video - Greška u izvoru - Pogreška remote-a - Pogreška renderera + Pogreška u izvoru + Pogreška eksternog računala + Pogreška u prikazu Neočekivana pogreška playera - Pogreška preuzimanja, provjeri dozvole za pohranu + Pogreška tijekom preuzimanja, provjeri dozvole za pohranu Chromecast epizoda Chromecast mirror Pokreni u aplikaciji @@ -257,11 +257,11 @@ Kopiraj poveznicu Automatsko preuzimanje Preuzmi zrcalo - Ponovno učitaj poveznice + Ponovo učitaj poveznice Preuzmi titlove - Oznaka kvalitete - Oznaka sinkronizacije - Oznaka titlova + Oznaka za kvalitetu + Oznaka za sinkronizaciju + Oznaka za titlove Naslov Uključi/isključi elemente korisničkog sučelja na posteru Nije pronađeno ažuriranje @@ -270,38 +270,38 @@ Promijeni veličinu Izvor Preskoči OP - Ne prikazuj više + Nemoj više prikazivati Preskoči ovo ažuriranje Ažuriraj - Preferirana kvaliteta streama + Preferirana kvaliteta gledanja (WiFi) Maksimalni broj znakova u naslovu video playera Rezolucija video playera - Veličina video međuspremnika - Duljina video međuspremnika - Video predmemorija na disku - Očisti predmemoriju videa i slika - Izazvat će nasumična rušenja ako se postavi previsoko. Nemojte mijenjati ako imate malu količinu RAM-a kao što je Android TV ili stari telefon. - Može uzrokovati probleme na sustavima s malo prostora za pohranu kao što su Android TV uređaji ako postavite previsoko. + Veličina međuspremnika videa + Duljina međuspremnika videa + Predmemorija videa na disku + Izbriši predmemoriju videa i slika + Uzrokuje rušenje aplikacije ako se postavi previsoko na uređajima s malom količinom RAM-a kao što je Android TV. + Uzrokuje probleme ako se postavi previsoko na uređajima s malom količinom memorije kao što je Android TV. DNS preko HTTPS-a Korisno za zaobilaženje blokada ISP-a Kloniraj web stranicu Ukloni web stranicu Dodajte klon postojeće web-lokacije s drugim url-om - Put preuzimanja + Putanja preuzimanja NGINX server URL Prikaži sinkronizirani anime ili s titlovima - Prilagodi zaslonu + Prilagodi veličini ekrana Rastegni Zoom - Obavijest + Pravna obavijest Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. Općenito - Random gumb - Prikaži gumb za slučajni odabir reprodukcija na početnoj stranici i biblioteci + Gumb za slučajni odabir + Prikaži gumb za slučajni odabir na početnoj stranici i biblioteci Jezici proširenja Izgled aplikacije Preferirani mediji - Omogućava NSFW na podržanim proširenjima + Omogući NSFW na podržanim proširenjima Kodiranje titlova Pružatelji usluga Raspored @@ -320,60 +320,60 @@ 127.0.0.1 NovoImeStranice https://primjer.com - Šifra jezika (en) + Šifra jezika (hr) %1$s %2$s račun Odjava Prijava Promijeni račun Dodaj račun - Napravi račun - Dodaj tracking + Stvori račun + Dodaj praćenje Dodano %s Sinkroniziraj Ocijenjeno %d / 10 /\?\? /%d - Ovjereno%s + %s ovjeren Nije moguće prijaviti se na %s Nijedan - Normal + Normalno Sve - Maksimalno - Minimalno - Obrub - Depresivno + Maks. + Min. + Kontura + Udubljeno Sjena - Podignuto + Izdignuto Sinkroniziraj titlove 1000 ms Kašnjenje titlova - Koristi ovo ako su titlovi prikazani %d ms prerano + Koristi ovo ako se titlovi prikazuju %d ms prerano Koristite ovo ako se titlovi prikazuju %d ms prekasno - Nema kašnjenja titlova + Bez kašnjenja titlova - The quick brown fox jumps over the lazy dog + Gojazni đačić s biciklom drži hmelj i finu vatu u džepu nošnje Preporučeno Učitano %s - Učitaj datoteku titlova - Učitaj sa interneta + Učitaj iz datoteke + Učitaj s interneta Preuzeta datoteka - Glavno - Podupiranje - Pozadina + Glavni + Sporedni + Statist Izvor - Random - Dolazi uskoro… - Cam - Cam - Cam + Slučajno + Dolazi uskoro … + Kamera + Kamera + Kamera HQ HD TS @@ -392,59 +392,59 @@ Rezolucija i naslov Naslov Rezolucija - ID je nevažeći + Nevažeći ID Nevažeći podaci - URL je nevažeći - Greška - Ukloni CC iz titlova - Ukloni reklame iz titlova - Filtriraj po željenom jeziku medija - Extras + Nevažeći URL + Pogreška + Ukloni titlove za gluhe osobe iz titlova + Ukloni nepotrebne elemente iz titlova (npr. oglase) + Filtriraj po preferiranom jeziku medija + Dodatni sadržaji Trailer https://primjer.com/primjer.mp4 - Referent (nije obavezno) + Referent (opcionalno) Sljedeće - Gledaj videozapise na ovim jezicima + Gledaj videa na ovim jezicima Prethodno Preskoči postavljanje - Promijeni izgled aplikacije kako bi odgovarao vašem uređaju + Promijeni izgled aplikacije kako bi odgovarao tvom uređaju Izvještavanje o rušenju - Što želite vidjeti + Što želiš vidjeti Gotovo - Ekstenzije - Dodaj repository - Ime repositorya - URL spremišta (repositorija) + Proširenja + Dodaj repozitorij + Ime repozitorija + URL repozitorija Dodatak učitan Dodatak izbrisan Nije moguće učitati %s 18+ - Započeto preuzimanje %1$d %2$s… + Započeto preuzimanje %1$d %2$s … Preuzeto %1$d %2$s - Sve %s je već preuzeto + Sve %s već preuzeto Skupno preuzimanje dodatak dodaci Ovo će također izbrisati sve dodatke repozitorija - Izbriši repository - Preuzmi popis stranica koje želite koristiti + Izbriši repozitorij + Preuzmi popis stranica koje želiš koristiti Preuzeto: %d Onemogućeno: %d Nepreuzeto: %d - CloudStream nema instalirane web stranice prema zadanim postavkama. Morate instalirati stranice iz repozitorija. + CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. \n \nPridružite se našem Discordu ili tražite online. - Pregledajte repozitorije zajednice + Prikaži repozitorije zajednice Javni popis - Svi titlovi pisani velikim slovima - Preuzeti sve dodatke iz ovog repozitorija\? - %s (Onemogućeno) - Zapis + Koristi velika slova za sve titlove + Preuzeti sve dodatke iz ovog repozitorija? + %s (onemogućeno) + Zapisi Audio zapis Video zapis - 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. + Za prikaz promjena ponovo pokreni aplikaciju. + Sigurnosni način rada uključen + Sva proširenja su isključena zbog rušenja aplikacije kako bi se pronašlo proširenje koje uzrokuje probleme. Pogledajte podatke o padu Ocjena: %s Opis @@ -454,38 +454,38 @@ Autori Podržano Jezik - HLS Playlista + HLS playlista Automatski instaliraj dodatke Zasluge Automatski instaliraj sve neinstalirane dodatke iz dodanih repozitorija. Preferirani video player Interni player - Prvo instalirajte ekstenziju + Najprije instaliraj proširenje VLC MPV - Web Video Cast + Emitiranje na webu Aplikacija nije pronađena Svi jezici Previše teksta. Nije moguće spremiti u međuspremnik. Označi kao gledano - Prikazuje skočni prozor za preskakanje početka ili završetka medija + Prikaži skočni prozor za uvod/kraj Da - Preuzimanje ažuriranja aplikacije… + Preuzimanje ažuriranja aplikacije … Jeste li sigurni da želite izaći\? Ne - Instaliranje ažuriranja aplikacije… + Instaliranje ažuriranja aplikacije … Nije moguće instalirati novu verziju aplikacije - Ažurirano %d dodataka - Mješoviti početak + Ažurirani dodaci: %d + Mješoviti uvod Uvod - Linkovi - Pokreni Trailer + Poveznice + Pokreni trailer Ponovi postupak postavljanja - Neki telefoni ne podržavaju novi program za instaliranje paketa. Isprobaj naslijeđenu opciju ako se ažuriranja ne instaliraju. + Neki telefoni ne podržavaju novi program za instaliranje paketa. Pokušaj sa starijom opcijom ako se ažuriranja ne instaliraju. Instalator APK-a Ažuriranja aplikacije Sigurnosna kopija - Ekstenzije + Proširenja Radnje Predmemorija Geste @@ -493,21 +493,21 @@ Titlovi Raspored Zadane postavke - Izgled + Izgledi Značajke Web preglednik Preskoči %s - Završetak - Zaključak - Mješoviti završetak - Obriši povijest + Kraj + Sažetak + Mješoviti kraj + Izbriši povijest Povijest Legacy - Otvaranje + Uvod PackageInstaller %1$s %2$d%3$s Aktualiziranje započeto - Program če se aktualizirati tijekom zatvaranja programa + Aplikacija će se aktualizirati tijekom zatvaranja Dodatak preuzet Ukloni iz pogledanog Preglednik @@ -525,19 +525,19 @@ Vaša je biblioteka prazna :( \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. - Prikazan player- iznos preskakanja - Količina preskakanja koja se koristi kada je player vidljiv - Player skriven - Količina preskakanja - Količina preskakanja koja se koristi kada je player skriven + Pronađena je datoteka sigurnog načina rada! +\nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. + Prikazan player – Količina pomicanja + Količina pomicanja koja se koristi kada je player vidljiv + Player skriven – Količina pomicanja + Količina pomicanja koja se koristi kada je player skriven Android TV - Prošlo - Restart + Uspjelo + Pokreni ponovo Log - Početak - Neuspješno - Stop + Pokreni + Neuspjelo + Prekini Test pružatelja usluga Ažuriranje pretplaćenih emisija Epizoda %d izbačena! @@ -549,7 +549,7 @@ GitHub Proxy Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy … Zaobilazi blokiranje neobrađenih GitHub URL-ova koristeći jsDelivr. Može uzrokovati kašnjenje ažuriranja nekoliko dana. - Preferirana kvaliteta gledanja (podatkovna mobilna mreža) + Preferirana kvaliteta gledanja (mobilni podaci) Profil %d Wi-Fi Mobilni podaci @@ -560,20 +560,20 @@ Pomoć Kvalitete Pozadina profila - Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s + Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA POGREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka Onemogući 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. + Repozitorij nije pronađen. Provjeri URL i pokušaj 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 prioritet videa. \n \nIzvor A: 3 \nKvaliteta B: 7 -\nImat će kombinirani prioritet videozapisa od 10. +\nImat će kombinirani prioritet videa 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 + Učestalost spremanja sigurnosne kopije %s uklonjeno iz favorita Favoriti %s dodano u favorite @@ -606,7 +606,7 @@ Preskoči odabir računa pri pokretanju Upravljanje računima Uredi račun - Linkovi ponovno učitani + Poveznice su ponovo učitane Rotiraj Prikaži gumb za prebacivanje orijentacije zaslona Omogućuje automatsko mijenjanje orijentacije zaslona na temelju orijentacije videa @@ -614,8 +614,8 @@ rotiraj_video_tipka automatski_rotiraj_video_tipka Obavijest za novu epizodu - Pretraži u ostalim proširenjima - Dodaje opciju brzine u playeru + Traži u drugim proširenjima + Dodaje opciju za brzinu u playeru Testiraj sva proširenja Ovaj je test namijenjen samo programerima i ne provjerava niti negira rad bilo kojeg proširenja. Prikaži preporuke @@ -624,9 +624,31 @@ Zaključaj s biometrijskim podatcima %s \npreostalo - Greška u pristupanju međuspremnika. Pokušaj ponovo. + Pogreška pri pristupanju međuspremnika. Pokušaj ponovo. Otključaj CloudStream Lozinka/PIN autentifikacija Ovaj uređaj ne podržava biometrijsku autentifikaciju Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo. - + U redu + Deaktiviraj optimizaciju baterije + Audio knjiga + Medij + Korištenje baterije aplikacije već je postavljeno na neograničeno + Neuspjelo otvaranje podataka CloudStream aplikacije. + Favorit + Ukloni iz favorita + Glazba + Obnovi + Otključaj aplikaciju pomoću otiska prsta, ID-a lica, PIN-a, uzorka i lozinke. + Sljedeća u %s + Pogreška pri kopiranju. Kopirajte zapisnik i kontaktirajte podršku aplikacije. + Kako bi se osigurala neometana preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom gumba „U redu” bit ćete preusmjereni na informacije o aplikaciji. Tamo odaberite „Korištenje baterije aplikacije” i postavite potrošnju baterije na „Neograničeno”. Imajte na umu da ovo dopuštenje ne znači da će CS3 isprazniti vašu bateriju. Radit će u pozadini samo kada je potrebno, kao što je primanje obavijesti ili preuzimanje videa sa službenih proširenja. Ako odlučite otkazati, ovu postavku možete prilagoditi kasnije u „Opće postavke”. + Vaši CloudStream podaci su sada spremljeni u sigurnosnu kopiju. Iako je vjerojatnost mala, neki se uređaji mogu ponašati drugačije. Ako izgubite pristup aplikaciji, potpuno izbrišite podatke aplikacije i obnovite ih pomoću sigurnosne kopije. Ispričavamo se zbog mogućih neugodnosti. + Sezona %1$d epizoda %2$d izlazi + Cast mirror + Fcast + Odaberite uređaj za emitiranje + CloudStream Wiki + Računi + Sigurnost + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 5533cdc0..717495a9 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -365,7 +365,7 @@ https://példa.hu/példa.mp4 Nem sikerült betölteni: %s Elkezdődött a(z) %1$d %2$s letöltése… - Töltse le az összes bővítményt ebből a tárolóból\? + Töltse le az összes bővítményt ebből a tárolóból? Biztonságos mód bekapcsolva Méret MPV @@ -592,4 +592,4 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása - + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index d537a1d5..b570068c 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -428,7 +428,7 @@ Ganti subtitle jadi huruf besar semua Terunduh: %d Tidak terunduh: %d - Unduh semua plugin dari repositori ini\? + Unduh semua plugin dari repositori ini? Semua Umur %s (Tidak aktif) Trek @@ -638,4 +638,13 @@ Buku Audio Media Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. - + Mengatur ulang + Musim %1$d Episode %2$d akan dirilis pada + Akan datang di %s + Cermin Cast + Pilih perangkat cast + Fcast + CloudStream Wiki + Keamanan + Akun + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 040b0f31..1341b146 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -239,7 +239,7 @@ Errore del renderer Errore inaspettato nel player video Errore download, controlla i permessi di archiviazione - Chromecast + Episodio Chromecast Mirror Chromecast Riproduci in app Riproduci in %s @@ -427,7 +427,7 @@ Vedi le repository della community Lista pubblica Tutti i sottotitoli in maiuscolo - Scaricare tutti i plugin da questa repository\? + Attenzione: CloudStream 3 non si assume alcuna responsabilità per l\'utilizzo di estensioni di terze parti e non fornisce alcun supporto per esse! %s (Disabilitato) Tracce Traccia audio @@ -619,7 +619,7 @@ Autenticazione con password/PIN L\'autenticazione biometrica non è supportata su questo dispositivo Sblocca app con impronta digitale, Face ID, PIN, sequenza e password. - Questa schermata è stata chiusa a causa di più tentativi falliti. Riavvia l\'app. + Dopo alcuni tentativi falliti, il prompt si chiuderà. Riavvia semplicemente l\'app per riprovare. È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo. Non preferito %s @@ -637,4 +637,21 @@ L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" Musica Audiolibro - + Reimposta + Prossimamente tra %s + L\'episodio %2$d della stagione %1$d uscirà tra + Mirror cast + Seleziona dispositivo per cast + Fcast + Wiki di CloudStream + Conti + Sicurezza + Autenticazione locale + Immagine codice QR + Respingi + Apri repository + Visita %s sul tuo smartphone o computer e inserisci il codice sopra + Impossibile ottenere il codice PIN del dispositivo, prova l\'autenticazione locale + Il codice PIN è scaduto! + Il codice scadrà tra %1$dm %2$ds + \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index da2952a0..2af7c967 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -442,7 +442,7 @@ לא ניתן להתקין את הגרסה החדשה של האפליקציה הורדת אצווה תוסף - הורד את כל התוספים ממאגר זה\? + הורד את כל התוספים ממאגר זה? רצועות שמע מסלולים Web Video Cast @@ -550,4 +550,4 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! - + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index acb2cfc3..5c80d77e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,4 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 - + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1a63050a..a8756d83 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -133,7 +133,7 @@ 백업 중 오류 %s 검색 라이브러리 - 계정 + 계정 및 보안 소스별로 구분된 검색 결과를 제공합니다 예고편 보기 Kitsu에서 포스터 보기 @@ -311,7 +311,7 @@ 커뮤니티 저장소 보기 공개 목록 모든 자막 대문자화 - 이 저장소에서 모든 플러그인을 다운로드하시겠습니까\? + 이 저장소에서 모든 플러그인을 다운로드하시겠습니까? %s (사용불가) 저장소 추가 저장소 이름 @@ -338,7 +338,7 @@ 로드된 백업 파일 정보 고급 검색 - 데이터를 보내지 않음 + 데이터를 보내지 않습니다 설정 프로세스 다시 실행 APK 인스톨러 Github @@ -527,4 +527,111 @@ 구독중 구독 %s 구독 취소 %s - + 보안 + 장부 + 리포지토리에서 플러그인을 찾을 수 없습니다 + 복사됨! + 레포지토리 이름 및 URL + 본 테스트는 개발자만을 대상으로 하며, 확장자의 작업을 확인하거나 거부하지 않습니다. + 클라우스스트림 위키 + 다시 기록된 링크 + 백업 빈도 + 즐겨찾기 + QR 이미지 + 모든 확장프로그램 테스트 + 로컬 인증 + 클립보드에 액세스하는 중 오류가 발생했습니다. 다시 시도하십시오. + 취소 + 저장소 열기 + 현재 PIN 입력 + 비디오 방향에 따라 화면 방향을 자동으로 전환합니다 + 장치 PIN 코드를 가져올 수 없습니다, 로컬 인증을 시도하세요 + PIN 코드가 만료되었습니다! + 코드 만료까지 남은 시간: %1$dm %2$ds + 리포지토리를 찾을 수 없습니다. URL을 확인하고 VPN을 시도하십시오 + 이미 투표했습니다 + UI를 올바르게 만들 수 없습니다. 이것은 주요 버그이며 %s 즉시 보고해야 합니다 + 즐겨찾기에 추가 + 와이파이 + 도움 + 품질 + 편집 + 프로필 + 확인 + 배터리 최적화 사용 안 함 + 앱 배터리 사용량이 이미 무제한으로 설정되었습니다 + CloudStream의 App 정보를 열 수 없습니다. + 즐겨찾기에 %s 추가 + 프로필 %d + 프로필 배경 + 대체 + PIN 입력 + PIN + PIN은 4자여야 합니다 + %s으로 로그인 됨 + 시작 시 계정 선택 건너뛰기 + 즐겨찾기 + 즐겨찾기 해제 + 잠금 해제 + 생체 인식으로 잠금 + 음악 + 오디오책 + 자동 회전 + 모바일 데이터 + 사용 불가능 + fcast + 캐스트 장치 선택 + 복사하는 중 오류가 발생했습니다. 로그캣을 복사하고 문의하십시오. + 구독 취소 + 기본값 설정 + 구독 + 사용 + 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 전부 대체 + 추가 + 즐겨찾기에서 %s 제거 + 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: +\n +\n%s +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 계정 선택 + 기본 계정 사용 + 회전 + 화면 방향을 전환할 토글 버튼 표시 + 계정 관리 + 프로필 잠금 + 잘못된 PIN입니다. 다시 시도하세요. + 계정 편집 + 미디어 + 비밀번호/PIN 인증 + 이 장치에서는 생체 인식이 지원되지 않습니다 + 지문, 얼굴 ID, PIN, 패턴 또는 비밀번호로 앱을 잠급니다. + 여러 번 실패하면 프롬프트가 닫힙니다. 다시 시도하려면 앱을 다시 시작하세요. + 재설정 + 플러그인 다운로드를 필터링할 모드 선택 + 데이터가 백업되었습니다. 장치에 따라 동작이 다를 수 있으며 앱 접근이 차단될 경우 앱 데이터를 완전히 지우고 백업에서 복원하세요. 이로 인해 발생하는 불편을 사과드립니다. + 스마트폰이나 컴퓨터에서 %s를 방문하여 위의 코드를 입력하세요 + 구독 TV 프로그램에 대한 중단 없는 다운로드 및 알림을 보장하기 위해 CloudStream은 백그라운드에서 실행할 수 있는 권한이 필요합니다. 확인을 누르면 App info로 이동합니다. 거기서 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚로 스크롤하여 배터리 사용량을 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙로 설정합니다. 이 권한은 CS3가 배터리를 소모한다는 의미가 아닙니다. 알림을 받거나 공식 확장에서 동영상을 다운로드하는 등 필요할 때만 백그라운드에서 작동합니다. 취소를 선택한 경우 나중에 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨에서 이 설정을 조정할 수 있습니다. + 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. +\n +\n참고 A: 3 +\n품질 B: 7 +\n총 비디오 우선 순위는 10입니다. +\n +\n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! + 시즌 %1$d 에피소드 %2$d이(가) 출시됩니다 + 다른 확장자에서 검색 + 새로운 에피소드 알림 + 권장 사항 표시 + 플레이어에 속도 옵션을 추가합니다 + %s로 출시 예정 + %s +\n남음 + 잠재적 중복 발견 + %s의 PIN 입력 + 즐겨찾기에서 제거 + 캐스트미러 + \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 49b333e3..7989654a 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -415,7 +415,7 @@ Skatīt kopienas krātuves Publisks saraksts Visi subtitri ar lielajiem burtiem - Vai lejupielādēt visus spraudņus no šīs krātuves\? + Vai lejupielādēt visus spraudņus no šīs krātuves? %s (atspējots) Tracks Audio dziesmas @@ -527,4 +527,4 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - + \ No newline at end of file diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index fe82a90b..05fc0900 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -89,7 +89,7 @@ Отстранете ги црните граници Преводи Поставки на плеерот за преводи - Режим на Eigengravy + Брзина на репродукција Повлечете за да барате Повлечете од страна на страна за да ја контролирате вашата позиција во видеото Повлечете за да ги промените поставките @@ -186,7 +186,7 @@ Зумирај Disclaimer Општи поставки - Јазици на провајдерите + Јазици на екстензиите Распоред на апликацијата Претпочитани медиуми Автоматски @@ -239,7 +239,7 @@ Видео Исчисти Положен - MyCoolSite + Име на сајт Неважечки податоци Поддршка Функции на плеерот @@ -250,11 +250,11 @@ Опис Апликацијата ќе се ажурира по излегувањето Отпишана е од %s - прокси raw.githubusercontent.com + GitHub прокси TC Претплатен на %s Преводи - Да се преземат сите приклучоци од ова складиште\? + Да се преземат сите приклучоци од ова складиште? Недостасуваат дозволи за складирање. Обидете се повторно. Зачувај Вчитај од датотека @@ -299,7 +299,7 @@ MPV Инсталатор на пакети ОВА - Ажурирања и резервни копии + Ажурирање и резервна копија Вашата библиотека е празна :( \nНајавете се на корисничка сметка или додадете серии. Не се пронајдени епизоди @@ -337,7 +337,7 @@ Додатоци Прикажи случајно копче на почетната страница и библиотеката Поддржано - Сметки + Сметки и безбедност Вовед Креирај сметка Отстрани од гледаното @@ -349,7 +349,7 @@ Ажурирани %d приклучоци Мешано отворање Екстензии - Овозможете NSFW на поддржани провајдери + Овозможете NSFW на поддржани екстензии Не успеа да стигне до GitHub. Вклучувам jsDelivr прокси… Филтрирајте по претпочитан медиумски јазик @string/home_play @@ -381,7 +381,7 @@ Прикажи постери од Kitsu Дали сте сигурни дека сакате да излезете\? Предизвикува проблеми ако е превисоко поставено на уреди со мал простор за складирање, како што е Android TV. - Користејќи jsDelivr, блокирањето на GitHub може да се заобиколи. Може да ги одложи ажурирањата за неколку дена. + Заобиколете го блокирањето на необработени URL-адреси на github користејќи jsDelivr. Може да предизвика ажурирањата да се одложат за неколку дена. Да Азбучно (Ш до А) WP @@ -398,7 +398,7 @@ Инсталатор на APK Екстензии UHD - Референт + Референт (опционално) Се отвора 127.0.0.1 Ова исто така ќе се избрише сите приклучоци за складиште @@ -409,7 +409,7 @@ Не успеа да ги врати податоците од датотеката %s Не успеа Документарец - Стрим + Мрежен проток %d мин Играј со CloudStream Пушти трејлер @@ -433,7 +433,7 @@ %dm \nпреостанува Видео кеш на дискот - Поврзување до пренос + https://example.com/example.mp4 Готово Додај складиште 18+ @@ -449,7 +449,7 @@ SDR Веб-прелистувач Апликацијата не е пронајдена - MyCoolUsername + Корисничко име Отвори со %1$s %2$d%3$s Повторете го процесот на поставување @@ -480,9 +480,7 @@ Приклучокот е преземен Не може да се вчита %s Преземете ја листата на сајтови што сакате да ги користите - CloudStream нема стандардно инсталирани локации. Треба да ги инсталирате сајтовите од складиштата. -\n -\nПоради отстранување на DMCA без мозок од страна на Sky UK Limited 🤮 не можеме да ја поврземе локацијата на складиштето во апликацијата. + CloudStream нема стандардно инсталирани екстензии. Треба сами да инсталирате екстензии. \n \nПридружете се на нашиот Discord или барајте онлајн. Песни @@ -507,9 +505,9 @@ Завршува Измешан крај HDR - example.com + https://example.com Синхронизирај преводи - Примени при рестартирање + Рестартирајте ја апликацијата за да ги видите промените. Наслов на видео плеер максимални знаци Увезете фонтови ставајќи ги во %s Врати ги податоците од резервна копија @@ -591,4 +589,39 @@ Зачестеност на зачувување на бекап Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација Автоматска ротација - + Име и URL на складиштето + копирано! + Тестирај ги сите екстензии + ОК + Користењето на батеријата на апликацијата е веќе поставено на неограничено + Неомилен + Омилен + Заклучување со биометрика + Музика + Известување за нова епизода + Пребарајте во други екстензии + Прикажи препораки + Додава опција за брзина во плеерот + Cast mirror + Овој тест е наменет само за програмери и не ја потврдува или негира работата на која било екстензија. + Не може да се отворат информациите за апликацијата CloudStream. + Лозинка/ПИН автентикација + Отклучете ја апликацијата со отпечаток од прст, ID на лице, PIN, шема и лозинка. + Сега е направена резервна копија на вашите податоци на CloudStream. Иако можноста за ова е многу мала, сите уреди можат да се однесуваат поинаку. Во ретки случаи, кога ќе се заклучите од пристап до апликацијата, целосно исчистете ги податоците на апликацијата и вратете ги од резервна копија. Многу ни е жал за какви било непријатности што произлегуваат од ова. + Ресетирај + Сезона %1$d Епизода %2$d ќе биде објавена за + Fcast + Одбери уред да кастираш + Оневозможи оптимизација на батерија + Отклучи CloudStream + Биометриската автентикација не е поддржана на овој уред + Овој екран беше затворен поради повеќе неуспешни обиди. Ве молиме рестартирајте ја апликацијата. + Медиуми + Претстои во %s + %s +\nпреостанати + За да обезбеди непрекинато преземања и известувања за претплатени ТВ-серии, на CloudStream му треба дозвола да работи во заднина. Со притискање на ОК, ќе бидете упатени до информации за апликацијата. Таму, дојдете до 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и поставете ја употребата на батеријата на Неограничено. Ве молиме имајте предвид, оваа дозвола не значи дека CS3 ќе ви ја испразни батеријата. Ќе работи само во заднина кога е потребно, како на пример при примање известувања или преземање видеа од официјални екстензии. Ако изберете да откажете, може да ја прилагодите оваа поставка подоцна во 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + Грешка при пристапот до таблата со исечоци, обидете се повторно. + Грешка при копирање, копирајте го logcat и контактирајте со поддршката за апликацијата. + Аудио книга + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 279f5511..0a0f7bd7 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -167,7 +167,7 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ് + %1$d ദിവസങ്ങൾ %2$d മണിക്കൂർ %3$d മിനിറ്റ് അധ്യായം%dൽ റിലീസ് ചെയ്യും %1$d മണിക്കൂർ %2$d മിനിറ്റ് %1$sഅധ്യാ%2$d @@ -280,4 +280,4 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം - + \ No newline at end of file diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 0c90b0c2..8170a7ff 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -55,4 +55,6 @@ Kongsi Tetapan Tutup - + Ep + cuba + \ No newline at end of file diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index ef796f9f..4bf2a273 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -417,7 +417,7 @@ ဖြည့်စွက်များ ရီပိုစစ်ထရီ ဖြည့်စွက်များအားလုံးကိုဖျက်မည်ဖြစ်သည် ရီပိုစစ်ထရီ ကိုဖျက်ရန် - ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား\? + ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား? %s (ပိတ်ပြီး) ထောက်ပံ့ထားသော ဘာသာစကား @@ -550,4 +550,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - + \ No newline at end of file diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 1e23f8af..99694e91 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -53,7 +53,7 @@ डाउनलोड रद्द गरियो डाउनलोड भयो अपडेट सुरु - स्ट्रिम + नेटवर्क स्ट्रीम लिङ्क लोड गर्दा त्रुटि भयो लिङ्कहरू रिलोड गरियो भित्री स्टोरेज @@ -70,7 +70,7 @@ बुकमार्कहरू फिल्टर गर्नुहोस् बुकमार्कहरू हटाउनुहोस् - हेरेको स्थिति राख्नुहोस् + हेरेको स्थिति निर्धारण गर्नुहोस् कपी बन्द खाली गर्नुहोस् @@ -85,4 +85,47 @@ स्रोतहरू स्वचालित बग रिपोर्टिङ असक्षम गर्नुहोस् लागू गर्नुहोस् - + साइट ले मेटाडाटा दिएको छैन,मेटाडाटा बिना भिडियो लोड नहुन सक्छ। + प्रकरण %1$d प्रसङ्ग %2$d प्रशारण हुनेवाला छ + प्रोभाईडर उपयोग गरी खोज्नुहोस् + भाषा डाउनलाेड गर्नुहोस् + उपशीर्षकको भाषा + यो प्रोभाईडर torrent हो त्यसैले VPN प्रयाेग गर्नुहुन सिफारिश गरिन्छ + वर्णन + केही विषय भेटिएन + Chromecast को उपशीर्षकहरु + केहीपनि वर्णन भेटिएन + Logcat देखाउनुहोस + Log + Picture-in-picture + अरु एप माथी सानो प्लेयरमा पलेब्याक जारी राख्दछ + प्लेयर स्पीड + उपशीर्षकको सेटिङ + विन्डोको रंग + उपशीर्षक ऊंचाई + अक्षरको नाप + फन्ट + प्रकारको उपयोग गरी खोज्नुहोस् + %d केरा डेभलपर लाई दिइयो + एउटै पनी केरा दिइएन + हेर्न सुचारु राख्नुहोस + हटाउनुहोस् + कालो सीमा हटाउनुहोस + उपशीर्षक + नयाँ प्रसङ्ग को सूचना + अन्य एक्सटेन्सन मा खोज्नुहोस् + सुुझाव हरु + अक्षरको रंग + बाहिरी रेखा को रंग + पृष्ठभूमिको रंग + धारको प्रकार + भाषा अटो छनौट + रिसेट गर्न स्क्रिनमा थिचिराख्नुहोस् + फन्ट देखाउन %s मा राख्नुहोस् + अधिक जानकारी + यो प्रोभाईडर सही ढंगले प्रयोग गर्न VPN प्रयोग गर्नुपर्ने हुन सक्छ + प्लेयर resize गर्ने वटन + प्लेयरको उपशीर्षकको सेटिङ + रिपोजिटरी को नाम र यूआरएल + कपी गरियो! + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index fc537837..8844407a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -487,7 +487,7 @@ Uitbreidingen Intro Publieke lijst - Alle plugins uit deze repository downloaden\? + Alle plugins uit deze repository downloaden? Beoordeling: %s Alle extensies zijn uitgeschakeld door een crash om u te helpen degene te vinden die problemen veroorzaakt. Bekijk de crash info @@ -608,4 +608,4 @@ Link opnieuw geladen Autoroteer Roteer - + \ No newline at end of file diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 724f4a63..b1168c36 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -335,7 +335,7 @@ Last ned listen over sider du vil bruke Dette vil også slette alle pakkebrønnsprogramtillegg Vis gemenskapspakkebrønner - Last ned alle programtilleggene fra denne pakkebrønnen\? + Last ned alle programtilleggene fra denne pakkebrønnen? %s (avskrudd) Spor Fant ikke programmet @@ -538,4 +538,4 @@ Bruk Hjelp Profilbakgrunn - + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c61f0104..4980c235 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -400,7 +400,7 @@ Zobacz repozytoria społeczności Publiczna lista Wszystkie napisy wielką literą - Pobrać wszystkie rozszerzenia z tego repozytorium\? + Uwaga: CloudStream 3 nie ponosi żadnej odpowiedzialności za korzystanie z rozszerzeń innych dostawców i nie zapewnia dla nich żadnego wsparcia! %s (Wyłączone) Ścieżki Ścieżki audio @@ -597,7 +597,7 @@ Ten test jest przeznaczony wyłącznie dla programistów i nie weryfikuje ani nie zaprzecza działaniu żadnego rozszerzenia. Zablokuj za pomocą biometrii Uwierzytelnianie hasłem/kodem PIN - Ten ekran został zamknięty z powodu wielu nieudanych prób. Uruchom ponownie aplikację. + Po kilku nieudanych próbach monit zostanie zamknięty. Aby spróbować ponownie, po prostu uruchom ponownie aplikację. Odblokuj CloudStream To urządzenie nie obsługuje uwierzytelniania biometrycznego Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła. @@ -618,4 +618,21 @@ Multimedia Użycie akumulatora przez aplikację jest już ustawione na nieograniczone Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. - + Resetuj + Nadchodzące w %s + Odcinek %2$d sezonu %1$d wyjdzie za + Fcast + Wybierz urządzenie do transmisji + Mirror transmisji + Wiki CloudStream + Bezpieczeństwo + Konta + Uwierzytelniaj lokalnie + Obraz kodu QR + Nie można uzyskać kodu PIN urządzenia. Spróbuj uwierzytelnienia lokalnego + Kod PIN stracił ważność! + Kod wygasa za %1$dm %2$ds + Odrzuć + Otwórz repozytorium + Odwiedź %s na swoim smartfonie lub komputerze i wprowadź powyższy kod + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 06e2352c..999ebefb 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -379,7 +379,7 @@ Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas - Transferir todos os plugins deste repositório\? + Transferir todos os plugins deste repositório? %s (Desativado) Instalador APK %d min @@ -615,4 +615,10 @@ Multimédia Desativar a otimização da bateria Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. - + Reiniciar + Episódio %1$d Episódio %2$d vai ser lançado em + Por vir em + Fcast + Escolha o dispositivo + Transmitir + \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index d7da44b4..30804c4d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -59,7 +59,7 @@ Descărcare eșuată Descărcare anulată Descărcare finalizată - Stream + Stream de rețea Eroare la încărcarea linkurilor Stocare internă Dub @@ -142,7 +142,7 @@ Permisiunea de arhivare lipșe, vă rugăm să încercați din nou. Eroare de backup %s Căutare - Conturi și credite + Conturi și Securitate Actualizări și copii de rezervă Informații Căutare avansată @@ -255,8 +255,8 @@ Lungimea buffer-ului video Dimensiunea cache-ului video pe disc Ștergeți memoria cache de imagine și video - Provoacă blocaje dacă este setată la un nivel prea ridicat pe dispozitive cu memorie redusă, cum ar fi Android TV. - Cauzează probleme dacă este setat la un nivel prea ridicat pe dispozitive cu spațiu de stocare redus, cum ar fi Android TV. + Cauzează blocări dacă este setat prea mare pe dispozitive cu memorie redusă, cum ar fi Android TV. + Cauzează probleme dacă este setat prea mare pe dispozitive cu spațiu de stocare redus, cum ar fi Android TV. DNS peste HTTPS Folositor pentru evitarea blocajelor ISP Adaugați site-ul @@ -272,8 +272,8 @@ Orice probleme legale privind conținutul acestei aplicații ar trebui să fie rezolvate cu furnizorii și gazdele actuale de fișiere, întrucât noi nu suntem afiliați cu aceștia. În caz de încălcare a drepturilor de autor, vă rugăm să contactați direct părțile responsabile sau site-urile de streaming. Aplicația este destinată exclusiv utilizării educaționale și personale. CloudStream 3 nu găzduiește niciun fel de conținut în aplicație și nu are niciun control asupra conținutului media care este pus sau retras. CloudStream 3 funcționează ca orice alt motor de căutare, cum ar fi Google. CloudStream 3 nu găzduiește, nu încarcă și nu gestionează niciun videoclip, film sau conținut. Pur și simplu navighează, adună și afișează linkuri într-o interfață convenabilă și ușor de utilizat. Pur și simplu, acesta extrage paginile web ale unor terțe părți care sunt accesibile publicului prin intermediul oricărui browser web obișnuit. Este responsabilitatea utilizatorului de a evita orice acțiune care ar putea încălca legile care guvernează locația sa. Utilizați CloudStream 3 pe propria răspundere. General Aleatoriu - Afișați butonul aleatoriu pe pagina de start și în bibliotecă - Limba furnizorului + Afișează butonul pentru aleatoriu pe Pagina Principală și în Bibliotecă + Limbi ale extensiei Aplicație de prezentare Media preferată Codificarea subtitrărilor @@ -309,7 +309,7 @@ /\?\? /%d %s autentificat/ă - Nu s-a putut autentifica la %s + Nu am putut să mă autentific la %s Nu există Normal @@ -332,7 +332,7 @@ https://en.wikipedia.org/w/index.php?title=Pangram&oldid=225849300 https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog --> - Vând muzică de jazz și haine de bun-gust în New-York și Quebec la preț fix. + Vulpea maro iute sare peste câinele leneș Recomandări A fost încărcat %s Încărcați din fișier @@ -343,7 +343,7 @@ Secundar Sursa Aleatoriu - În curând + În curând… Cam Cam Cam @@ -365,7 +365,7 @@ Titlu și rezoluție Titlu Rezoluție - ID invalid + ID-ul invalid Date invalide Eroare @@ -394,14 +394,14 @@ %1$d %2$s NSFW %1$d-%2$d - Player Afișat - Căutați Suma - Player Ascuns/ă - Căutați Suma + Jucătorul afișat - Cantitatea de căutare + Jucător ascuns - Sumă de căutare Livestream-uri NSFW Eșuat - Suma căutată și utilizată atunci când player-ul este vizibil/ă + Suma de căutare utilizată atunci când jucătorul este vizibil Livestream - Cantitatea de căutare utilizată atunci când playerul este ascuns + Cantitatea de căutare folosită când jucătorul este ascuns Calitatea preferată (Date Mobile) Video Instalator APK @@ -426,11 +426,11 @@ Dezabonat de la %s Nu s-a descărcat: %d Vezi depozite din comunitate - PackageInstaller (Instalare a pachetelor) + Instalator de pachete Stare Nu se poate încărca %s Piste audio - Referent + Referer (opțional) Deschidere Extensii Layout @@ -440,7 +440,8 @@ Autori Raportarea accidentelor Adaugă depozit - Se pare că biblioteca ta este goală :( Conectează-te la un cont de bibliotecă sau adaugă emisiuni în biblioteca ta locală. + Biblioteca ta este goală :( +\nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. Eliminați subtitrările închise din subtitrări Descărcați lista de site-uri pe care doriți să le utilizați Evaluare (Ridicat la Scăzut) @@ -453,7 +454,7 @@ Vezi informații despre accident Deschideți cu Eliminați bloat din subtitrări - Actualizat %d plugin-uri + S-au actualizat %d plugin-uri Evaluare (Scăzut la Ridicat) Terminat Versiune @@ -472,11 +473,11 @@ Sortează Selectați Biblioteca Filtrați în funcție de limba media preferată - Episodul %d lansat! + Episodul %d a fost lansat! Android TV VLC Urmăriți videoclipuri în aceste limbi - Reveniți + Revenire Acțiuni Alfabetic (Z la A) URL invalid @@ -492,7 +493,7 @@ \nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. Scoateți de la urmărit Actualizat (Vechi la Nou) - Aplică la repornire + Reporniți aplicația pentru a vedea schimbările. Descriere Plugin Descărcat Sunteți sigur că vreți să ieșiți\? @@ -508,16 +509,16 @@ Nu s-a putut instala noua versiune a aplicației Piste Repornește - Activează NSFW la furnizori suportate + Activează conținutul pentru adulți pe extensiile suportate Nu s-a putut ajunge la GitHub. Se activează proxy-ul jsDelivr… Proxy GitHub - Depășește blocarea GitHub folosind jsDelivr. Poate cauza întârzieri de câteva zile la actualizări. + Ocolește blocarea URL-urilor brute de pe GitHub folosind jsDelivr. Poate cauza întârzieri în actualizări cu câteva zile. Următorul Toate %s deja descărcate - S-a descărcat: %d + Descărcat: %d Dezactivat: %d Toate subtitrările cu majuscule - Descărcați toate plugin-urile din acest depozit\? + Descărcați toate plugin-urile din acest depozit? Se actualizează emisiunile abonate Abonat Lista publică @@ -532,7 +533,7 @@ Suportat Playlist HLS Piste video - Arată Afișați pop-up-uri de săritură pentru deschidere/încheiere + Afișează opțiunea de omitere a ferestrelor pop-up pentru început/sfârșit Toate limbile Deschidere mixat Credite @@ -593,4 +594,51 @@ Adaugă o opțiune de viteză la player Favoriți/te Frecvența de backup - + Numele și URL-ul depozitului + Copiat! + Eroare la accesarea Clipboard-ului. Te rog să încerci din nou. + Eroare la copiere. Te rog să copiezi logcat-ul și să contactezi suportul aplicației. + PIN incorect. Te rog să încerci din nou. + PIN + Selectați un cont + Administrați conturile + Editare cont + Conectat ca %s + Rotire + Nefavorite + Deblocați CloudStream + Omiteți selecția contului la pornire + Linkuri reîncărcate + Utilizați contul implicit + Această testare este destinată doar dezvoltatorilor și nu verifică sau respinge funcționarea oricărei extensii. + Pentru a asigura descărcările neîntrerupte și notificările pentru serialele TV la care ești abonat, CloudStream are nevoie de permisiunea de a rula în fundal. Apăsând pe OK, vei fi direcționat către informațiile aplicației. Acolo, derulează la \"App battery usage\" și setează utilizarea bateriei la \"Unrestricted\". Te rog să reții, această permisiune nu înseamnă că CS3 îți va consuma bateria. Va opera în fundal doar când este necesar, cum ar fi atunci când primește notificări sau descarcă videoclipuri din extensiile oficiale. Dacă alegi să anulezi, poți ajusta această setare mai târziu în \"General Settings\". + PIN-ul trebuie să fie format din 4 caractere + Afișează un buton de comutare pentru orientarea ecranului + Autentificare parolă/PIN + Autentificarea biometrică nu este acceptată pe acest dispozitiv + Deblocați aplicația cu amprentă digitală, ID facial, PIN, model și parolă. + Acest ecran a fost închis din cauza mai multor încercări eșuate. Vă rugăm să reporniți aplicația. + Datele dvs. CloudStream au fost salvate acum. Deși posibilitatea acestui lucru este foarte mică, toate dispozitivele se pot comporta diferit. În cazul rar, în care nu aveți acces la aplicație, ștergeți complet datele aplicației și restaurați dintr-o copie de rezervă. Ne pare foarte rău pentru orice neplăcere care decurge din aceasta. + Ok + Dezactivează optimizarea bateriei + Utilizarea bateriei pentru aplicație este deja setată ca fiind nelimitată + Imposibil de deschis informațiile aplicației CloudStream. + Favorite + Muzică + Carte audio + Media + Caută în alte extensii + Testează toate extensiile + Rotire automată + Resetați + Activați comutarea automată a orientării ecranului pe baza orientării video + Blocare cu biometrie + %s +\nrămase + Următorul în %s + CloudStream Wiki + Sezonul %1$d Episod %2$d va fi lansat în + Selectați divece-ul pe care doriți să faceți cast + Cast mirror + Fcast + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cf456f56..79aa66e1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -422,6 +422,7 @@ %s (отключено) Далее В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. +\n \nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. Недопустимые данные Разрешение и название @@ -450,7 +451,7 @@ Все %s уже скачаны Начата загрузка %1$d %2$s… Не скачано: %d - Скачать все плагины из этого репозитория\? + Скачать все плагины из этого репозитория? Включен безопасный режим Скачано: %d Обновлено %d плагинов @@ -616,4 +617,9 @@ Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. - + Сброс + Сезон %1$d Эпизод %2$d выйдет + Выйдет %s + Fcast + Выберите девайс для трансляции + \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ebaaa2ae..5ba29a00 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -112,7 +112,7 @@ Sledujem Popis sa nenašiel Ďalší náhodný - Stream + Sieťový stream Podržané Zobraziť Logcat 🐈 Protokol @@ -355,4 +355,26 @@ Maximálny počet znakov v názve prehrávača Spôsobuje problémy, ak je nastavená príliš vysoko v zariadeniach s malým ukladacím priestorom, ako je napríklad Android TV. Frekvencia zálohovania - + Toto tiež odstráni všetky doplnky repozitára + Sezóna %1$d Epizóda %2$d bude vydaná za + V repozitári neboli nájdené žiadne doplnky + Sťahuje sa aktualizácia aplikácie… + Inštaluje sa aktualizácia aplikácie… + skopírované! + Názov a URL repozitára + Aktualizujú sa odoberané relácie + Upozornenie na novú epizódu + Repozitár nebol nájdený, skontrolujte URL adresu a skúste VPN + Odkazy sa znovu načítali + Zmazať repozitár + URL adresa repozitára + Verejný zoznam + CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. +\n +\nPripojte sa k nášmu Discord alebo vyhľadajte online. + Nepodarilo sa nainštalovať novú verziu aplikácie + Stiahnuť všetky doplnky z tohto repozitára? + Pridať repozitár + Názov repozitára + Zobraziť komunitné repozitáre + \ No newline at end of file diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index 7b0d2870..90198dd5 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -440,7 +440,7 @@ \nSababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited🤮, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno... \n \nDiscord naga soo qabo ama internetka ka baadh. - Soo deji dhamaan sidkanayaasha reboositarkan\? + Soo deji dhamaan sidkanayaasha reboositarkan? Boodhka Boodhka xalqadda Boodhka weyn @@ -485,4 +485,4 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka - + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 76508c43..695cbd31 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -33,7 +33,7 @@ Försök ansluta igen… Gå tillbaka @string/result_poster_img_des - Spela Avsnitt + Spela avsnitt Ladda ner Intern lagring Dub @@ -71,7 +71,7 @@ Håll inne för att återställa till standard Fortsätt titta Ta bort - Mer information + Mer info En VPN kan behövas för att den här leverantören ska fungera korrekt Denna leverantör är en torrent, en VPN rekommenderas Beskrivning @@ -162,12 +162,12 @@ Visa inte igen Uppdatera Nerladdningar startad - Nerladdning Misslyckades + Nerladdning misslyckades Nerladdad Laddar ner - Nerladdning Pausad - Nerladdning Avbryten - Nerladdning Färdig + Nerladdning pausad + Nerladdning avbryten + Nerladdning färdig Återupta nerladdning Pausa nerladdning Pausa @@ -204,7 +204,7 @@ Logga ut konto Nerladdningsplats - Tittar på nytt + Ser om Automatisk DNS över HTTPS " " @@ -217,7 +217,7 @@ Dubbeltryck i mitten för att pausa Återställ data från backup Konton och säkerhet - Uppdateringar och backup + Uppdateringar och säkerhetskopiering Automatiska pluginuppdateringar %1$dd %2$dh %3$dm Sök %s… @@ -230,7 +230,7 @@ Autospela nästa episod Spela Trailer Starta nästa episod när nuvarande slutar - Episod %d kommer släppas om + Episod %d kommer att släppas om %d min Visa trailers @string/home_play @@ -366,7 +366,7 @@ Titta på videor på dessa språk Föregående Spår - Uppdatering påbörjad + Uppdatering startad Logg Videospelarens hoppsträcka (Sekunder) Ändra status @@ -436,7 +436,7 @@ All %s har redan laddats ner Ladda ner alla tillägg från den här databasen? Felsäkert läge på - Applicera vid omstart + Starta om appen för att se ändringar. Intern spelare Kamera HD Tillägg nedladdad @@ -460,7 +460,7 @@ Avsnitt %d släppt! Den här listan är tom. Försök byta till en annan. Tillägg borttagen - Tillägg laddade + Tillägg laddad Tillägg Säkerhetskopierings antal Uppdatera visnings förlopp @@ -490,7 +490,7 @@ Web Affischbild Vad vill du se - Lägg till databas + Lägg till tillägg Uppdaterade %d tillägg Nedladdat %1$d %2$s Inaktiverad: %d @@ -524,7 +524,7 @@ Förbikoppla ISP Kamera Kamera - Alla tillägg stängdes av på grund av en krasch för att hjälpa dig hitta den som orsakar problem. + Alla tillägg stängdes av på grund av en krasch för att hjälpa dig hitta det tillägget som orsakar problem. Storlek Författarna Stödd @@ -599,9 +599,31 @@ Lås upp appen med Fingerprint, Face ID, PIN, mönster eller lösenord. Lås upp CloudStream Biometrisk autentisering stöds inte på den här enheten - Detta fönster stängs efter några misslyckade försök. Du måste starta om appen. + Skärmen stängdes av på grund av flera misslyckade försök. Starta om applikationen. Favorit Ta bort från favoriter %s \nkvarstår - + kopierad! + Tilläggs namn och URL + För att säkerställa oavbrutna nedladdningar och aviseringar för prenumererade tv-program behöver CloudStream tillstånd att köras i bakgrunden. Genom att trycka på OK kommer du till App info. Där bläddrar du till appens batterianvändning och ställer in batterianvändningen på obegränsad. Observera att denna behörighet inte betyder att CS3 kommer att tömma ditt batteri. Den fungerar bara i bakgrunden när det behövs, till exempel när du tar emot aviseringar eller laddar ner videor från officiella tillägg. Om du väljer att avbryta kan du ändra denna inställning senare i allmänna inställningar. + Din CloudStream-data har säkerhetskopierats nu. Även om möjligheten till detta är mycket liten, kan alla enheter bete sig olika. I det sällsynta fallet att du blir utelåst från att komma åt appen, rensa appdata helt och återställ från en säkerhetskopia. Vi ber om ursäkt för eventuella besvär som detta uppstår. + Ljudbok + Det gick inte att komma åt urklipp. Försök igen. + OK + Inaktivera batterioptimering + Appens batterianvändning är redan inställd på obegränsad + Det gick inte att öppna CloudStreams appinformation. + Musik + Återställ + Kommer ut om %s + Fel vid kopiering, kopiera logcat och kontakta appsupport. + Media + Fcast + Cast mirror + Säsong %1$d Avsnitt %2$d kommer att släppas om + Välj cast-enhet + CloudStream Wiki + Konton + Säkerhet + \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index e981d05a..4b000304 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -10,17 +10,17 @@ மேலும் விருப்பங்கள் அடுத்த அத்தியாயம் வகைகள் - பகிர் + பங்கு உலாவியில் திற - ஏற்றுவதைத் தவிர் + ஏற்றுவதைத் தவிர்க்கவும் பார்த்து கொண்டிருப்பது - நிறுத்தி வைக்கப்பட்டுள்ளது - நிறைவடைந்தது + ஆன்-ஓல்ட் + முடிந்தது பார்க்கத் திட்டமிடப்பட்டுள்ளது மீண்டும் பார்க்கத் தொடங்கியது - ஸ்ட்ரீம் டோரண்ட் + ச்ட்ரீம் டொரண்ட் வசன வரிகள் - பின் செல் + திரும்பிச் செல்லுங்கள் அத்தியாயத்தை இயக்கு எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும் பதிவிறக்கப்பட்டது @@ -28,82 +28,82 @@ பதிவிறக்கம் இடைநிறுத்தப்பட்டது பதிவிறக்கம் தொடங்கியது பதிவிறக்கம் தோல்வியடைந்தது - ஸ்ட்ரீம் + பிணையம் ச்ட்ரீம் உள் சேமிப்பு மொழிபெயர்க்கப்பட்டது - கோப்பை நீக்கு - கோப்பை இயக்கவும் - பதிவிறக்கத்தை நிறுத்து + கோப்பை அழி + கோப்பு + இடைநிறுத்தம் பதிவிறக்கம் தானியங்கி பிழை அறிக்கையை முடக்கு - மேலும் தகவல்கள் + மேலும் செய்தி மறை - நீக்கு - நீக்கு - சேமிக்கவும் - உரை வண்ணம் + அகற்று + தெளிவான + சேமி + உரை நிறம் வெளிப்புற நிறம் பின்னணி நிறம் - வசன உயர்வு + வசன உயரம் எழுத்துரு வழங்குபவர்கள் பயன்படுத்தி தேடுங்கள் - வகைகளைப் பயன்படுத்தி தேடவும் + வகைகளைப் பயன்படுத்தி தேடுங்கள் மொழிகளைப் பதிவிறக்கவும் வசன மொழி - தொடர்ந்து பார்க்கவும் + தொடர்ந்து பார்த்துக் கொள்ளுங்கள் முறையாக இயங்க vpn பயன்படுத்தவும் - பிளேயர் அளவை மாற்றும் பொத்தான் - Chromecast வசனங்கள் - அமைப்புகளை மாற்ற ஸ்வைப் செய்யவும் - அடுத்த எபிசோடை தானாக இயக்கவும் - தற்போதைய அத்தியாயம் முடிந்ததும் அடுத்த அத்தியாயத்தைத் தொடங்கவும் - தேடுவதற்கு இருமுறை தட்டவும் - பிளேயரில் தேடுதல் வேகம் - இடைநிறுத்துவதற்கு நடுவில் தட்டவும் + பிளேயர் மறுஅளவிடுதல் பொத்தானை + Chromecast வசன வரிகள் + அமைப்புகளை மாற்ற ச்வைப் செய்யவும் + தன்னியக்க அடுத்த அத்தியாயம் + தற்போதைய ஒன்று முடிவடையும் போது அடுத்த அத்தியாயத்தைத் தொடங்கவும் + தேட இரட்டை தட்டு + வீரர் தொகை (விநாடிகள்) + இடைநிறுத்த நடுவில் இரண்டு முறை தட்டவும் நடிகர்கள்: %s - பின் செல் + திரும்பிச் செல்லுங்கள் அமைப்புகள் ஏற்றுகிறது… கைவிடப்பட்டது பதிவிறக்கம் முடிந்தது இணைப்பை மீண்டும் முயலவும்… - திரைப்படத்தை இயக்கு - லைவ்ஸ்ட்ரீம் இயக்கு + திரைப்படம் திரைப்படம் + லைவ்ச்ட்ரீம் விளையாடுங்கள் டிரெய்லரை இயக்கு - மூலம் + மூலங்கள் இணைப்புகளை ஏற்றுவதில் பிழை - இயக்கு + விளையாடுங்கள் பதிவிறக்கம் ரத்து செய்யப்பட்டது வசன அமைப்புகள் - பதிவிறக்கத்தை மீண்டும் தொடங்கவும் + பதிவிறக்கத்தை மீண்டும் தொடங்குங்கள் புக்மார்க்குகளை வடிகட்டவும் தகவல் - பிளேயர் வேகம் - புக்மார்க்கு - பயன்படுத்து - நகலெடுக்கவும் + பிளேயர் விரைவு + புக்மார்க்குகள் + இடு + நகலெடு மூடு எழுத்துரு அளவு - நீக்கு - மேலும் தகவல்கள் - தானாக மொழியை தேர்ந்தெடு - முன்னோக்கி அல்லது பின்னோக்கி தேட வலது அல்லது இடது பக்கத்தில் இருமுறை தட்டவும் + அகற்று + மேலும் செய்தி + தானாக தேர்ந்தெடுக்கப்பட்ட மொழி + முன்னோக்கி அல்லது பின்னோக்கி தேட வலது அல்லது இடது பக்கத்தில் இரண்டு முறை தட்டவும் மொபைலில் பிரகாசத்தை பயன்படுத்த - இயல்புநிலைக்கு மீட்டமைக்க அழுத்திப் பிடிக்கவும் + இயல்புநிலைக்கு மீட்டமைக்க பிடிக்கவும் முறையாக இயங்க vpn பரிந்துரைக்கப்பட்டது கருப்பு எல்லைகளை அகற்றவும் - விளக்கம் + விவரம் கதை எதுவும் காணப்படவில்லை - விளக்கம் ஏதும் காணப்படவில்லை - படத்தில்-படம் - பிளேயர் வசனங்கள் அமைப்புகள் - Logcat 🐈 காட்டு - பிற பயன்பாடுகளுக்கு மேல் மினியேச்சர் பிளேயரில் பிளேபேக் தொடர்கிறது + எந்த விளக்கமும் கிடைக்கவில்லை + படம்-படம் + பிளேயர் வசன வரிகள் அமைப்புகள் + LOGCAT ஐக் காட்டு + மற்ற பயன்பாடுகளின் மேல் ஒரு மினியேச்சர் பிளேயரில் பிளேபேக்கைத் தொடர்கிறது வசன வரிகள் - வீடியோ பிளேயரில் நேரத்தைக் கட்டுப்படுத்த இடது அல்லது வலதுபுறம் ஸ்வைப் செய்யவும் - பிரகாசம் அல்லது ஒலியளவை மாற்ற இடது அல்லது வலது பக்கத்தில் ஸ்வைப் செய்யவும் - இடைநிறுத்துவதற்கு இருமுறை தட்டவும் - Chromecast வசன அமைப்புகள் - இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும் + ஒரு வீடியோவில் உங்கள் நிலையைக் கட்டுப்படுத்த பக்கத்திலிருந்து பக்கமாக ச்வைப் செய்யவும் + ஒளி அல்லது அளவை மாற்ற இடது அல்லது வலது பக்கத்தில் மேலே அல்லது கீழே சறுக்கி விடுங்கள் + இடைநிறுத்த இரட்டை தட்டு + Chromecast வசன வரிகள் அமைப்புகள் + இருண்ட மேலடுக்கு பதிலாக ஆப் பிளேயரில் கணினி பிரகாசத்தைப் பயன்படுத்தவும் அத்தியாயம் %d-இன் வெளியீட்டு நேரம் %1$dம %2$dநி %dநி @@ -118,5 +118,501 @@ எபிசோட்டின் போஸ்டர் போஸ்டர் பிரதான போஸ்டர் - %1$s Ep %2$d - + %1$s ep %2$d + %S ஏற்ற முடியவில்லை + %1$dd %2$dh %3$dm + வசன வரிகள் + முடிவு + முடிந்தது + சுயவிவரம் %d + வைஃபை + மொழி குறியீடு (en) + பதிவு + டப் சிட்டை + வாட்ச் முன்னேற்றத்தைப் புதுப்பிக்கவும் + ஏற்றப்பட்ட காப்புப்பிரதி கோப்பு + விரிவாக்க மொழிகள் + கணக்குகள் மற்றும் பாதுகாப்பு + எச்.எல்.எச் பிளேலிச்ட் + கலப்பு முடிவு + கிளவுட்ச்ட்ரீம் + பெனின்கள் எதுவும் கொடுக்கப்படவில்லை + பிளேபேக் விரைவு + தேட ச்வைப் + தரவை காப்புப் பிரதி எடுக்கவும் + மேம்பட்ட தேடல் + செருகுநிரல்களை தானாக பதிவிறக்கவும் + %1$d-%2$d + +30 + இது %s நிரந்தரமாக நீக்கும +\n நீ சொல்வது உறுதியா? + ஆண்டு + எதிர்பாராத பிளேயர் பிழை + பயன்பாட்டில் விளையாடுங்கள் + Chromecast அத்தியாயம் + ஆண்ட்ராய்டு டிவி போன்ற குறைந்த சேமிப்பு இடங்களைக் கொண்ட சாதனங்களில் மிக அதிகமாக அமைக்கப்பட்டால் சிக்கல்களை ஏற்படுத்துகிறது. + செயல்கள் + தலைப்பை சுவரொட்டியின் கீழ் வைக்கவும் + விரைவில் வருகிறது… + வீடியோ தடங்கள் + சிக்கலை ஏற்படுத்தும் ஒரு விபத்து காரணமாக அனைத்து நீட்டிப்புகளும் அணைக்கப்பட்டன. + சொருகி பதிவிறக்கம் செய்யப்பட்டது + பயன்பாடு கிடைக்கவில்லை + பிளேயர் காட்டப்பட்டுள்ளது - தொகையைத் தேடுங்கள் + பயன்பாட்டு புதுப்பிப்பைப் பதிவிறக்குகிறது… + கிட்அப்பை அடைய முடியவில்லை. Jsdelivr ப்ராக்சியை இயக்குதல்… + நிறுத்து + Chromecast கண்ணாடி + Hello@world.com + தோல்வி + /%d + லைவ்ச்ட்ரீம் + களஞ்சிய பெயர் மற்றும் முகவரி + நகலெடுக்கப்பட்டது! + நகலெடுப்பதில் பிழை, தயவுசெய்து LogCat ஐ நகலெடுத்து பயன்பாட்டு ஆதரவை தொடர்பு கொள்ளவும். + கிளிப்போர்டை அணுகுவதில் பிழை, மீண்டும் முயற்சிக்கவும். + அகரவரிசை (A முதல் சட் வரை) + %S க்கு முள் உள்ளிடவும் + தற்போதைய முள் உள்ளிடவும் + முள் + தவறான முள். தயவு செய்து மீண்டும் முயற்சிக்கவும். + தலைப்பு + சொருகி நீக்கப்பட்டது + தளம் + பாதுகாப்பான பயன்முறை + கொடுக்கப்பட்ட பெனீன் + திரைப்படங்கள் + இல்லை + முரண்பாட்டில் சேரவும் + ஆசிய நாடகங்கள் + மதிப்பிடப்பட்டது: %.1 எஃப் + \@string/home_play + செயல்வரம்பு + தேடல் + டிரெய்லர்களைக் காட்டு + இணைப்புகள் எதுவும் கிடைக்கவில்லை + கிளிப்போர்டில் இணைப்பு நகலெடுக்கப்பட்டது + கள் + அனைத்தும் + வசன நேரந்தவறுகை இல்லை + அதிக உரை. கிளிப்போர்டில் சேமிக்க முடியவில்லை. + பார்த்தபடி குறி + மற்றவைகள் + இணைப்புகள் மீண்டும் ஏற்றப்பட்டன + துணை + சாளரம் நிறம் + எழுத்துருக்களை %s இல் வைப்பதன் மூலம் இறக்குமதி செய்யுங்கள் + மேனிலை தரவு தளத்தால் வழங்கப்படவில்லை, தளத்தில் இல்லாவிட்டால் வீடியோ ஏற்றுதல் தோல்வியடையும். + காப்பு அதிர்வெண் + தரவு சேமிக்கப்பட்டது + சேமிப்பக அனுமதிகள் இல்லை. தயவு செய்து மீண்டும் முயற்சிக்கவும். + செயலிழப்புகள் குறித்த தரவை மட்டுமே அனுப்புகிறது + சுவரொட்டியில் இடைமுகம் கூறுகளை மாற்றவும் + மேம்படுத்தல் சோதிக்க + பூட்டு + அனிமேசன் டப்பிங்/துணை + சைகைகள் + விரைவான பழுப்பு நரி சோம்பேறி நாய் மீது குதிக்கிறது + மூலம் + கேம் + சுவரொட்டி படம் + செயலிழப்பு அறிக்கை + பொது பட்டியல் + பதிப்பு + நூலகத்தைத் தேர்ந்தெடுக்கவும் + பதிவு + கணக்கைத் திருத்தவும் + %1$s %2$d %3$s + ரத்துசெய் + இயல்புநிலை + ஓவா + டொரண்ட் + ஆவணப்படம் + ஆசிய நாடகம் + NSFW + மூலம் + விருப்பமான கண்காணிப்பு தகுதி (மொபைல் தரவு) + பெரிதாக்கு + ISP பைபாச் + நீட்டிப்புகள் + பிளேயர் நற்பொருத்தங்கள் + நற்பொருத்தங்கள் + பொது + ஆதரிக்கப்பட்ட நீட்டிப்புகளில் NSFW ஐ இயக்கவும் + முதன்மை நிறம் + பயன்பாட்டு கருப்பொருள் + சுவரொட்டி தலைப்பு இடம் + %கள் அங்கீகரிக்கப்பட்டவை + வசன நேரந்தவறுகை + வசன வரிகள் %d ms மிக விரைவாக காட்டப்பட்டால் இதைப் பயன்படுத்தவும் + பரிந்துரைக்கப்படுகிறது + ஏற்றப்பட்ட %s + கோப்பிலிருந்து ஏற்றவும் + இணையத்திலிருந்து ஏற்றவும் + ஆட்டக்காரர் + தீர்மானம் மற்றும் தலைப்பு + தலைப்பு + வசன வரிகளிலிருந்து மூடிய தலைப்புகளை அகற்றவும் + வசனங்களிலிருந்து வீக்கத்தை அகற்று + முந்தைய + நீட்டிப்புகள் + களஞ்சிய முகவரி + சொருகி ஏற்றப்பட்டது + %1$d %2$s ஐ பதிவிறக்கத் தொடங்கியது… + பதிவிறக்கம் %1$d %2$s + பதிவிறக்கம்: %d + அனைத்து %கள் ஏற்கனவே பதிவிறக்கம் செய்யப்பட்டுள்ளன + முடக்கப்பட்டது: %d + அனைத்து வசன வரிகள் + ஆடியோ தடங்கள் + மறுதொடக்கம் + விவரம் + ஆசிரியர்கள் + வலை வீடியோ நடிகர்கள் + இணைய உலாவி + %S ஐத் தவிர்க்கவும் + மறுபரிசீலனை செய்யுங்கள் + அறிமுகம் + வரலாற்றை அழிக்கவும் + அகரவரிசை (z முதல் A வரை) + உடன் திறந்திருக்கும் + குணங்கள் + கூட்டு + மாற்றவும் + அனைத்தையும் மாற்று + உங்கள் நூலகத்தில் ஏற்கனவே ஒரு நகல் உருப்படி இருப்பதாகத் தெரிகிறது: \'%கள்.\' +\n இந்த உருப்படியை எப்படியும் சேர்க்க விரும்புகிறீர்களா, இருக்கும் ஒன்றை மாற்ற விரும்புகிறீர்களா அல்லது செயலை ரத்து செய்ய விரும்புகிறீர்களா? + கடிகார நிலையை அமைக்கவும் + விளிம்பு வகை + பருவம் இல்லை + அத்தியாயம் + தற்குறிப்பு + -30 + ஒளிதோற்றம் + வீடியோ பிளேயர் தீர்மானம் + வீடியோ இடையக அளவு + நகலி தளம் + அறிவிலிமையம் பதிலாள் + JSdelivr ஐப் பயன்படுத்தி மூல அறிவிலிமையம் முகவரி களின் பைபாச். புதுப்பிப்புகள் சில நாட்களுக்கு தாமதமாகிவிடும். + களஞ்சியம் கிடைக்கவில்லை, முகவரி ஐ சரிபார்த்து VPN ஐ முயற்சிக்கவும் + தொகுதி பதிவிறக்கம் + சொருகு + இந்த களஞ்சியத்திலிருந்து அனைத்து செருகுநிரல்களையும் பதிவிறக்கவா? + மொழி + திரும்பவும் + %S இலிருந்து குழுவிலகப்பட்டது + இது அனைத்து களஞ்சிய செருகுநிரல்களையும் நீக்கிவிடும் + நீங்கள் பயன்படுத்த விரும்பும் தளங்களின் பட்டியலைப் பதிவிறக்கவும் + விருப்பமான வீடியோ பிளேயர் + திறப்பு/முடிவுக்கு ச்கிப் பாப்அப்களைக் காட்டு + பயன்படுத்தவும் + தொகு + NSFW + பிளேயர் மறைக்கப்பட்டுள்ளது - தொகையைத் தேடுங்கள் + Nginx சேவையக முகவரி + பின்னணி + மதிப்பீடு: %கள் + நிச்சயமாக நீங்கள் வெளியேற வேண்டுமா? + வீடியோ மற்றும் பட தற்காலிக சேமிப்பை அழிக்கவும் + பார்த்ததிலிருந்து அகற்று + APK நிறுவி + ஆண்ட்ராய்டு டிவி + அதிகபட்சம் + கேம் + சாதாரண + தடங்கள் + கிதப் + + வேறு முகவரி உடன் ஏற்கனவே இருக்கும் தளத்தின் குளோனைச் சேர்க்கவும் + Https க்கு மேல் dns + உங்கள் தற்போதைய அத்தியாயம் முன்னேற்றத்தை தானாக ஒத்திசைக்கவும் + சுருக்கம் + இந்த வழங்குநருக்கு Chromecast உதவி இல்லை + அடுத்தது + பிழை + பயன்பாட்டு தளவமைப்பு + இயல்புநிலை + டி.எச் + கணக்கு + வசன குறியீட்டு + பயன்பாட்டு மொழி + டிவி தளவமைப்பு + காப்புப்பிரதியிலிருந்து தரவை மீட்டெடுக்கவும் + பிழை %s + புதுப்பிப்புகள் மற்றும் காப்புப்பிரதி + PackactionInstaller + வெளியேறியதும் பயன்பாடு புதுப்பிக்கப்படும் + வரிசைப்படுத்தவும் + புதுப்பிக்கப்பட்டது (பழையது முதல் புதியது) + சுயவிவர பின்னணி + நீங்கள் ஏற்கனவே வாக்களித்து விட்டீர்கள் + பிடித்தவை + பிடித்தவைகளிலிருந்து அகற்று + சாத்தியமான நகல் காணப்படுகிறது + மாறாத + கிளவுட்ச்ட்ரீமைத் திறக்கவும் + பயோமெட்ரிக்சுடன் பூட்டு + ஆடியோ நூல் + புதிய அத்தியாயம் அறிவிப்பு + பிற நீட்டிப்புகளில் தேடுங்கள் + கிட்சுவிலிருந்து சுவரொட்டிகளைக் காட்டு + அத்தியாயம் விளையாடுங்கள் + இயல்புநிலை மதிப்புக்கு மீட்டமைக்கவும் + பருவம் + நிலை + இலவசம் + தொலைக்காட்சி தொடர் + அனிம் + மீண்டும் காட்ட வேண்டாம் + நீட்சி + அனைத்து நீட்டிப்புகளையும் சோதிக்கவும் + இந்த சோதனை டெவலப்பர்களுக்கு மட்டுமே, எந்தவொரு நீட்டிப்பையும் சரிபார்க்கவோ மறுக்கவோ இல்லை. + முன்மாதிரி தளவமைப்பு + பதிவிறக்கம் செய்யப்பட்ட கோப்பு + பகுத்தல் + எம்.பி.வி. + உங்கள் நூலகம் காலியாக உள்ளது :( +\n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். + குழுவிலகவும் + சுயவிவரங்கள் + முள் 4 எழுத்துகளாக இருக்க வேண்டும் + ஒரு கணக்கைத் தேர்ந்தெடுக்கவும் + கணக்குகளை நிர்வகிக்கவும் + செருகுநிரல்கள் + தவறான வலைதள முகவரி + தானியங்கி சொருகி புதுப்பிப்புகள் + பதிவிறக்கத்தை வடிகட்ட பயன்முறையைத் தேர்ந்தெடுக்கவும் + மீட்டமை + களஞ்சியத்தில் செருகுநிரல்கள் எதுவும் காணப்படவில்லை + சீசன் %1$d எபிசோட் %2$d வெளியிடப்படும் + புதுப்பிப்பு தொடங்கியது + பரிந்துரைகளைக் காட்டு + டெவ்சுக்கு வழங்கப்பட்ட %d பெனின்கள் + பிளேயரில் வேக விருப்பத்தை சேர்க்கிறது + கோப்பு %s இலிருந்து தரவை மீட்டெடுப்பதில் தோல்வி + நூலகம் + தகவல் + வழங்குநரால் பிரிக்கப்பட்ட தேடல் முடிவுகளை உங்களுக்கு வழங்குகிறது + தேடல் முடிவுகளில் தேர்ந்தெடுக்கப்பட்ட வீடியோ தரத்தை மறைக்கவும் + பயன்பாட்டு புதுப்பிப்புகளைக் காட்டு + பயன்பாட்டைத் தொடங்கிய பின் புதிய புதுப்பிப்புகளைத் தானாகவே தேடுங்கள். + அமைவு செயல்முறை மீண்டும் + முன்நிபந்தனைகளுக்கு புதுப்பிக்கவும் + அதே தேவ்சின் ஒளி நாவல் பயன்பாடு + தேவ்சுக்கு ஒரு பெனீன் கொடுங்கள் + %1$d %2$s + அத்தியாயங்கள் எதுவும் கிடைக்கவில்லை + அழி + கோப்பை அழி + இடைநிறுத்தம் + தொடங்கு + கடந்து சென்றது + %டி.எம +\n மீதமுள்ள + %கள +\n மீதமுள்ள + நடந்து கொண்டிருக்கிறது + காலம் + வரிசையில் + பயன்படுத்தப்பட்டது + பயன்பாடு + ஆவணப்படங்கள் + லைவ்ச்ட்ரீம்கள் + தொடர் + கார்ட்டூன் + அனிம் + பிழையைப் பதிவிறக்குங்கள், சேமிப்பக அனுமதிகளை சரிபார்க்கவும் + வழங்குநரை மாற்றவும் + முன்னோட்டம் பின்னணி + எந்த தரவை அனுப்பவில்லை + அனிமேசுக்கு நிரப்பு அத்தியாயத்தைக் காட்டு + கூடுதல் களஞ்சியங்களிலிருந்து இன்னும் நிறுவப்படாத அனைத்து செருகுநிரல்களையும் தானாக நிறுவவும். + முழு வெளியீடுகளுக்கு பதிலாக மட்டுமே புதுப்பிப்புகளைத் தேடுங்கள் + அதே தேவ்சின் அனிம் பயன்பாடு + அத்தியாயங்கள் + %S இல் வரவிருக்கும் + மன்னிக்கவும், விண்ணப்பம் செயலிழந்தது. ஒரு அநாமதேய பிழை அறிக்கை டெவலப்பர்களுக்கு அனுப்பப்படும் + முடிந்தது + வசன வரிகள் இல்லை + கார்ட்டூன்கள் + டொரண்ட்ச் + படம் + %S இல் விளையாடுங்கள் + மூல பிழை + தொலை பிழை + ரெண்டரர் பிழை + உலாவியில் விளையாடுங்கள் + இணைப்பை நகலெடுக்கவும் + ஆட்டோ பதிவிறக்கம் + கண்ணாடியைப் பதிவிறக்கவும் + இணைப்புகளை மீண்டும் ஏற்றவும் + OP ஐத் தவிர்க்கவும் + இந்த புதுப்பிப்பைத் தவிர்க்கவும் + வசன வரிகள் பதிவிறக்கவும் + துணை சிட்டை + மறுஅளவிடுங்கள் + விருப்பமான கடிகார தகுதி (வைஃபை) + வீடியோ பிளேயர் தலைப்பு மேக்ச் சார்ச் + தளத்தை அகற்று + பாதை பதிவிறக்க + வீடியோ இடையக நீளம் + வட்டில் வீடியோ கேச் + பிளேயர் தெரியும் போது பயன்படுத்தப்படும் தேடல் தொகை + பிளேயர் மறைக்கப்படும்போது பயன்படுத்தப்படும் தேடல் தொகை + ஆண்ட்ராய்டு டிவி போன்ற குறைந்த நினைவகம் கொண்ட சாதனங்களில் மிக அதிகமாக அமைக்கப்பட்டால் செயலிழப்புகளை ஏற்படுத்துகிறது. + ISP தொகுதிகளைத் தவிர்ப்பதற்கு பயனுள்ளதாக இருக்கும் + திரைக்கு பொருந்தும் + மறுப்பு + இணைப்புகள் + பயன்பாட்டு புதுப்பிப்புகள் + காப்புப்பிரதி + கேச் + மனையமைவு + தெரிகிறது + சீரற்ற பொத்தான் + முகப்புப்பக்கம் மற்றும் நூலகத்தில் சீரற்ற பொத்தானைக் காட்டு + வழங்குநர் சோதனை + மனையமைவு + விருப்பமான மீடியா + வழங்குநர்கள் + தானி + தொலைபேசி தளவமைப்பு + கடவுச்சொல் 123 + பயனர்பெயர் + 127.0.0.1 + %1$s %2$s + புகுபதிகை + கணக்கு சேர்க்க + உங்கள் கணக்கை துவங்குங்கள் + கண்காணிப்பைச் சேர்க்கவும் + சேர்க்கப்பட்டது %கள் + ஒத்திசைவு + மதிப்பிடப்பட்டது + %d / 10 + /?? + %S இல் உள்நுழைய முடியவில்லை + முடக்கு + எதுவுமில்லை + மணித்துளி + அவுட்லைன் + மனச்சோர்வு + நிழல் + எழுப்பப்பட்ட + ஒத்திசைவு துணை + வசன வரிகள் காட்டப்பட்டால் இதைப் பயன்படுத்தவும் %d ms மிகவும் தாமதமானது + முக்கிய + துணை + கேம் + டி.சி. + ப்ளூ-ரே + Wp + டிவிடி + 4 கே + எச்.டி. + யுஎச்.டி + எச்.டி.ஆர் + எச்டி + எச்.டி.ஆர் + தவறான ஐடி + தவறான தரவு + விருப்பமான ஊடக மொழியால் வடிகட்டவும் + கூடுதல் + டிரெய்லர் + https://example.com/example.mp4 + குறிப்பாளர் (விரும்பினால்) + இந்த மொழிகளில் வீடியோக்களைப் பாருங்கள் + சமூக களஞ்சியங்களைக் காண்க + %கள் (முடக்கப்பட்டவை) + மாற்றங்களைக் காண பயன்பாட்டை மறுதொடக்கம் செய்யுங்கள். + செயலிழப்பு தகவலைக் காண்க + களஞ்சியத்தை நீக்கு + கிளவுட்ச்ட்ரீமில் இயல்புநிலையாக எந்த தளங்களும் நிறுவப்படவில்லை. நீங்கள் களஞ்சியங்களிலிருந்து தளங்களை நிறுவ வேண்டும். +\n எங்கள் முரண்பாட்டில் சேரவும் அல்லது ஆன்லைனில் தேடுங்கள். + நிலை + அளவு + ஆதரிக்கப்பட்டது + முதலில் நீட்டிப்பை நிறுவவும் + Fcast + காச்ட் சாதனத்தைத் தேர்ந்தெடுக்கவும் + அனைத்து மொழிகளும் + ஆம் + பேட்டரி தேர்வுமுறை முடக்கு + உங்கள் நூலகத்தில் சாத்தியமான நகல் உருப்படிகள் கண்டறியப்பட்டுள்ளன: +\n %கள் +\n இந்த உருப்படியை எப்படியும் சேர்க்க விரும்புகிறீர்களா, ஏற்கனவே உள்ளவற்றை மாற்ற விரும்புகிறீர்களா அல்லது செயலை ரத்து செய்ய விரும்புகிறீர்களா? + முள் உள்ளிடவும் + பூட்டு சுயவிவரம் + %S ஆக உள்நுழைந்துள்ளது + தொடக்கத்தில் கணக்கு தேர்வைத் தவிர்க்கவும் + இயல்புநிலை கணக்கைப் பயன்படுத்தவும் + கடவுச்சொல்/முள் ஏற்பு + இந்த சாதனத்தில் பயோமெட்ரிக் ஏற்பு ஆதரிக்கப்படவில்லை + கைரேகை, முகம் ஐடி, முள், முறை மற்றும் கடவுச்சொல் மூலம் பயன்பாட்டைத் திறக்கவும். + பல தோல்வியுற்ற முயற்சிகள் காரணமாக இந்த திரை மூடப்பட்டது. விண்ணப்பத்தை மறுதொடக்கம் செய்யுங்கள். + காச்ட் மிரர் + புதுப்பிப்பு எதுவும் கிடைக்கவில்லை + செய்தித் பெயர் + https://example.com + கணக்கை மாற்றவும் + 1000 எம்.எச் + சீரற்ற + Hq + அமைப்பைத் தவிர்க்கவும் + உங்கள் சாதனத்திற்கு ஏற்றவாறு பயன்பாட்டின் தோற்றத்தை மாற்றவும் + உனக்கு என்ன பார்க்க வேண்டும் + களஞ்சியத்தைச் சேர்க்கவும் + களஞ்சிய பெயர் + 18+ + பதிவிறக்கம் செய்யப்படவில்லை: %d + புதுப்பிக்கப்பட்டது %d செருகுநிரல்கள் + உள் வீரர் + வி.எல்.சி. + திறப்பு + கலப்பு திறப்பு + வரவு + வரலாறு + சரி + கிளவுட்ச்ட்ரீமின் பயன்பாட்டுத் தகவலைத் திறக்க முடியவில்லை. + %S க்கு குழுசேர்ந்தது + உதவி + %கள் பிடித்தவைகளில் சேர்க்கப்படுகின்றன + பிடித்தவையில் சேர் + சுழற்றுங்கள் + திரை நோக்குநிலைக்கு மாற்று பொத்தானைக் காண்பி + வீடியோ நோக்குநிலையின் அடிப்படையில் திரை நோக்குநிலையின் தானியங்கி மாறுவதை இயக்கவும் + ஆட்டோ சுழலும் + பிடித்த + இசை + ஓவா + தரமான சிட்டை + புதுப்பிப்பு + விடுபதிகை + விரலிடைத் தோல் + சில தொலைபேசிகள் புதிய தொகுப்பு நிறுவியை ஆதரிக்கவில்லை. புதுப்பிப்புகள் நிறுவப்படாவிட்டால் மரபு விருப்பத்தை முயற்சிக்கவும். + சந்தா தொலைக்காட்சி நிகழ்ச்சிகளுக்கான தடையற்ற பதிவிறக்கங்கள் மற்றும் அறிவிப்புகளை உறுதிப்படுத்த, கிளவுட்ச்ட்ரீம் பின்னணியில் இயங்க இசைவு தேவை. சரி என்பதை அழுத்துவதன் மூலம், நீங்கள் பயன்பாட்டுத் தகவலுக்கு அனுப்பப்படுவீர்கள். அங்கு, 𝘼𝙥𝙥 𝘼𝙥𝙥 பெறுநர் க்கு உருட்டி, பேட்டரி பயன்பாட்டை 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 என அமைக்கவும். தயவுசெய்து கவனிக்கவும், இந்த இசைவு CS3 உங்கள் பேட்டரியை வெளியேற்றும் என்று அர்த்தமல்ல. அறிவிப்புகளைப் பெறும்போது அல்லது உத்தியோகபூர்வ நீட்டிப்புகளிலிருந்து வீடியோக்களைப் பதிவிறக்குவது போன்ற பின்னணியில் மட்டுமே இது செயல்படும். நீங்கள் ரத்து செய்ய தேர்வுசெய்தால், இந்த அமைப்பை பின்னர் 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 in இல் சரிசெய்யலாம். + பயன்பாட்டு பேட்டரி பயன்பாடு ஏற்கனவே கட்டுப்பாடற்றதாக அமைக்கப்பட்டுள்ளது + பயன்பாட்டு புதுப்பிப்பை நிறுவுகிறது… + பயன்பாட்டின் புதிய பதிப்பை நிறுவ முடியவில்லை + மரபு + வரிசைப்படுத்து + மதிப்பீடு (உயர் முதல் குறைந்த வரை) + மதிப்பீடு (குறைந்த முதல் உயர் வரை) + புதுப்பிக்கப்பட்டது (பழையது புதியது) + இந்த பட்டியல் காலியாக உள்ளது. இன்னொரு இடத்திற்கு மாற முயற்சிக்கவும். + பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! +\n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. + சந்தா காட்சிகளைப் புதுப்பித்தல் + சந்தா + எபிசோட் %d வெளியானது! + மொபைல் தரவு + இயல்புநிலையை அமைக்கவும் + ஆதாரங்கள் எவ்வாறு உத்தரவிடப்படுகின்றன என்பதை இங்கே மாற்றலாம். ஒரு வீடியோவுக்கு அதிக முன்னுரிமை இருந்தால், அது மூல தேர்வில் அதிகமாகத் தோன்றும். மூல முன்னுரிமையின் தொகை மற்றும் தரமான முன்னுரிமை ஆகியவை வீடியோ முன்னுரிமை. +\n சான்று A: 3 +\n தகுதி பி: 7 +\n 10 இன் ஒருங்கிணைந்த வீடியோ முன்னுரிமை இருக்கும். +\n குறிப்பு: தொகை 10 அல்லது அதற்கு மேற்பட்டதாக இருந்தால், அந்த இணைப்பு ஏற்றப்படும்போது பிளேயர் தானாகவே ஏற்றுவதைத் தவிர்க்கும்! + இடைமுகம் ஐ சரியாக உருவாக்க முடியவில்லை, இது ஒரு பெரிய பிழை மற்றும் உடனடியாக %கள் தெரிவிக்க வேண்டும் + %கள் பிடித்தவைகளிலிருந்து அகற்றப்பட்டன + உங்கள் கிளவுட்ச்ட்ரீம் தரவு இப்போது காப்புப் பிரதி எடுக்கப்பட்டுள்ளது. இதன் சாத்தியம் மிகக் குறைவு என்றாலும், எல்லா சாதனங்களும் வித்தியாசமாக நடந்து கொள்ளலாம். அரிய விசயத்தில், பயன்பாட்டை அணுகுவதிலிருந்து நீங்கள் பூட்டப்படுகிறீர்கள், பயன்பாட்டு தரவை முழுவதுமாக அழித்து, காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். இதிலிருந்து எழும் ஏதேனும் சிரமத்திற்கு நாங்கள் மிகவும் வருந்துகிறோம். + ஊடகம் + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c3e5959a..bd74194e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -18,10 +18,10 @@ %1$ds %2$dd %dm - Poster + Afiş Afiş - Bölüm Posteri - Ana Poster + Bölüm Afişi + Ana Afiş Sonraki Rastgele @string/play_episode Geri git @@ -62,7 +62,7 @@ Canlı yayını oynat Torrent oynat Kaynaklar - Alt yazılar + Altyazılar Yeniden bağlan… Geri dön Bölümü oynat @@ -80,7 +80,7 @@ Bağlantılar yüklenirken hata oluştu Dahili depolama Dublajlı - Alt yazılı + Altyazılı Dosyayı sil Dosyayı oynat İndirmeyi sürdür @@ -100,13 +100,13 @@ Temizle Kaydet Oynatıcı hızı - Alt yazı ayarları + Altyazı ayarları Yazı rengi Dış hat rengi Arka plan rengi Pencere rengi Kenar tipi - Alt yazı yüksekliği + Altyazı yüksekliği Yazı tipi Yazı boyutu Sağlayıcıları kullanarak ara @@ -116,7 +116,7 @@ Otomatik seçilecek dil İndirilecek diller Altyazı dili - Sıfırlamak için basılı tut + Varsayılana sıfırlamak için basılı tutun Fontları içe aktarmak için %s konumuna yerleştirin İzlemeye devam et Kaldır @@ -133,10 +133,10 @@ İçerik diğer uygulamaların üzerinde küçük bir pencerede oynatılmaya devam eder Oynatıcı yeniden boyutlandırma butonu Siyah sınır çizgilerini kaldır - Alt yazılar - Oynatıcı alt yazı ayarları - Chromecast alt yazıları - Chromecast alt yazı ayarları + Altyazılar + Oynatıcı altyazı ayarları + Chromecast altyazıları + Chromecast altyazı ayarları Oynatma hızı Atlamak için kaydır Zamanı ayarlamak için yanlardan kaydır @@ -220,7 +220,7 @@ Site Özet Sıraya alındı - Alt yazı yok + Altyazı yok Varsayılan Boş Kullanılan @@ -263,16 +263,16 @@ Otomatik indir Şu kaynaktan indir Bağlantıları yenile - Alt yazıları indir + Altyazıları indir Kalite etiketi Dublaj etiketi - Alt yazı etiketi + Altyazı etiketi Başlık show_hd_key show_dub_key show_sub_key show_title_key - Poster üzerindeki öğeler + Afiş üzerindeki öğeleri değiştir Güncelleme bulunamadı Güncellemeleri denetle Kilitle @@ -298,7 +298,7 @@ Farklı bir URL ile mevcut bir sitenin klonunu ekleyin İndirme konumu NGINX sunucu URL\'si - Dublajlı/Alt yazılı animeleri göster + Dublajlı/Altyazılı Anime Gösterimi Ekrana sığdır Uzat Yakınlaştır @@ -307,12 +307,12 @@ Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. Genel Rastgele İçerik - Anasayfada ve Kütüphanede rastgele düğmesini göster + Ana sayfada ve Kütüphanede rastgele düğmesini göster Uzantı dilleri Uygulama düzeni Tercih edilen medya Desteklenen Uzantılarda NSFW\'yi etkinleştirin - Alt yazı kodlaması + Altyazı kodlaması Sağlayıcılar Düzen Otomatik @@ -321,8 +321,8 @@ Emülatör düzeni Birincil renk Uygulama teması - Poster başlık konumu - Başlığı posterin altına yerleştir + Afiş başlık konumu + Başlığı afişin altına yerleştir anilist_key mal_key @@ -344,7 +344,7 @@ Trakt --> %1$s %2$s - hesabı + hesap Çıkış yap Giriş yap Hesap değiştir @@ -360,7 +360,7 @@ %s başarıyla doğrulandı %s ile giriş yapılamadı - Hiçbiri + Yok Normal Hepsi Maksimum @@ -370,12 +370,12 @@ Çökmüş Gölge Yükseltilmiş - Alt yazı senkronu + Altyazı senkronu 1000 ms - Alt yazı gecikmesi - Alt yazılar %d ms erken görüntüleniyorsa bunu kullanın - Alt yazılar %d ms geç gözüküyorsa bunu kullanın - Alt yazı gecikmesi yok + Altyazı gecikmesi + Altyazılar %d ms erken görüntüleniyorsa bunu kullanın + Altyazılar %d ms geç gözüküyorsa bunu kullanın + Altyazı gecikmesi yok Movie Series From 02b956940a626daaed1ba2af70954ac152324237 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 4 Jul 2024 18:07:01 +0000 Subject: [PATCH 107/157] Port large parts of the API to crossplatform (#1163) --- app/build.gradle.kts | 2 + .../lagradost/cloudstream3/AcraApplication.kt | 4 +- .../lagradost/cloudstream3/CommonActivity.kt | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 24 +- .../cloudstream3/network/CloudflareKiller.kt | 1 + .../cloudstream3/plugins/PluginManager.kt | 2 +- .../services/BackupWorkManager.kt | 2 +- .../services/SubscriptionWorkManager.kt | 4 +- .../syncproviders/AccountManager.kt | 13 +- .../syncproviders/{SyncAPI.kt => SyncApi.kt} | 10 - .../syncproviders/providers/AniListApi.kt | 2 +- .../syncproviders/providers/MALApi.kt | 2 +- .../providers/OpenSubtitlesApi.kt | 1 + .../syncproviders/providers/SimklApi.kt | 55 +--- .../cloudstream3/ui/ControllerActivity.kt | 2 +- .../cloudstream3/ui/WebviewFragment.kt | 2 +- .../cloudstream3/ui/account/AccountHelper.kt | 2 +- .../ui/download/DownloadAdapter.kt | 3 +- .../ui/download/DownloadButtonSetup.kt | 6 +- .../ui/download/DownloadFragment.kt | 3 +- .../cloudstream3/ui/home/HomeFragment.kt | 14 +- .../ui/home/HomeParentItemAdapter.kt | 2 +- .../ui/home/HomeParentItemAdapterPreview.kt | 2 +- .../cloudstream3/ui/home/HomeViewModel.kt | 10 +- .../ui/library/LibraryFragment.kt | 6 +- .../cloudstream3/ui/library/PageAdapter.kt | 4 +- .../ui/player/AbstractPlayerFragment.kt | 6 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 3 +- .../ui/player/DownloadFileGenerator.kt | 1 - .../ui/player/DownloadedPlayerActivity.kt | 4 + .../ui/player/ExtractorLinkGenerator.kt | 1 - .../ui/player/FullScreenPlayer.kt | 2 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 1 + .../cloudstream3/ui/player/IGenerator.kt | 1 - .../cloudstream3/ui/player/IPlayer.kt | 1 - .../cloudstream3/ui/player/LinkGenerator.kt | 20 +- .../ui/player/OfflinePlaybackHelper.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 1 - .../ui/player/PreviewGenerator.kt | 3 - .../ui/player/RepoLinkGenerator.kt | 1 - .../player/source_priority/PriorityAdapter.kt | 4 +- .../player/source_priority/ProfilesAdapter.kt | 4 +- .../ui/quicksearch/QuickSearchFragment.kt | 7 +- .../cloudstream3/ui/result/ActorAdaptor.kt | 2 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 2 +- .../cloudstream3/ui/result/ResultFragment.kt | 2 +- .../ui/result/ResultFragmentPhone.kt | 10 +- .../ui/result/ResultFragmentTv.kt | 9 +- .../ui/result/ResultViewModel2.kt | 108 +++++-- .../cloudstream3/ui/result/UiText.kt | 2 +- .../cloudstream3/ui/search/SearchFragment.kt | 14 +- .../cloudstream3/ui/search/SearchHelper.kt | 2 +- .../ui/search/SearchResultBuilder.kt | 2 +- .../ui/settings/SettingsAccount.kt | 6 +- .../ui/settings/SettingsProviders.kt | 6 +- .../settings/extensions/ExtensionsFragment.kt | 4 +- .../ui/settings/extensions/PluginAdapter.kt | 2 +- .../ui/settings/extensions/PluginsFragment.kt | 2 +- .../ui/settings/testing/TestResultAdapter.kt | 4 +- .../ui/settings/testing/TestView.kt | 2 +- .../ui/setup/SetupFragmentProviderLanguage.kt | 2 +- .../utils/{AppUtils.kt => AppContextUtils.kt} | 187 +++++++++-- .../cloudstream3/utils/DataStoreHelper.kt | 2 +- .../cloudstream3/utils/InAppUpdater.kt | 2 +- .../utils/PackageInstallerService.kt | 2 +- .../utils/VideoDownloadManager.kt | 11 +- library/build.gradle.kts | 9 + .../lagradost/api/ContextHelper.android.kt | 20 ++ .../network/WebViewResolver.android.kt | 43 +-- .../kotlin/com/lagradost/api/ContextHelper.kt | 16 + .../com/lagradost/cloudstream3/MainAPI.kt | 290 ++++-------------- .../cloudstream3/extractors/AStreamHub.kt | 2 +- .../cloudstream3/extractors/Acefile.kt | 0 .../cloudstream3/extractors/AsianLoad.kt | 0 .../cloudstream3/extractors/Blogger.kt | 0 .../cloudstream3/extractors/BullStream.kt | 0 .../cloudstream3/extractors/ByteShare.kt | 0 .../lagradost/cloudstream3/extractors/Cda.kt | 0 .../cloudstream3/extractors/Chillx.kt | 0 .../extractors/ContentXExtractor.kt | 2 +- .../cloudstream3/extractors/Dailymotion.kt | 0 .../cloudstream3/extractors/DoodExtractor.kt | 0 .../cloudstream3/extractors/EPlay.kt | 0 .../cloudstream3/extractors/Embedgram.kt | 0 .../extractors/EmturbovidExtractor.kt | 0 .../cloudstream3/extractors/Evolaod.kt | 0 .../cloudstream3/extractors/Fastream.kt | 0 .../cloudstream3/extractors/Filesim.kt | 0 .../cloudstream3/extractors/GMPlayer.kt | 0 .../cloudstream3/extractors/Gdriveplayer.kt | 0 .../cloudstream3/extractors/GenericM3U8.kt | 0 .../cloudstream3/extractors/Gofile.kt | 0 .../extractors/GoodstreamExtractor.kt | 0 .../cloudstream3/extractors/GuardareStream.kt | 0 .../extractors/HDMomPlayerExtractor.kt | 2 +- .../extractors/HDPlayerSystemExtractor.kt | 2 +- .../extractors/HDStreamAbleExtractor.kt | 0 .../extractors/HotlingerExtractor.kt | 0 .../cloudstream3/extractors/Hxfile.kt | 0 .../cloudstream3/extractors/JWPlayer.kt | 0 .../cloudstream3/extractors/Jawcloud.kt | 0 .../cloudstream3/extractors/Jeniusplay.kt | 0 .../cloudstream3/extractors/Krakenfiles.kt | 0 .../cloudstream3/extractors/Linkbox.kt | 0 .../cloudstream3/extractors/M3u8Manifest.kt | 0 .../extractors/MailRuExtractor.kt | 2 +- .../cloudstream3/extractors/Maxstream.kt | 0 .../cloudstream3/extractors/Mediafire.kt | 0 .../cloudstream3/extractors/Minoplres.kt | 0 .../cloudstream3/extractors/MixDrop.kt | 0 .../cloudstream3/extractors/Moviehab.kt | 0 .../cloudstream3/extractors/Mp4Upload.kt | 0 .../cloudstream3/extractors/MultiQuality.kt | 0 .../cloudstream3/extractors/Mvidoo.kt | 0 .../extractors/OdnoklassnikiExtractor.kt | 2 +- .../cloudstream3/extractors/OkRuExtractor.kt | 0 .../cloudstream3/extractors/Okrulink.kt | 0 .../extractors/PeaceMakerstExtractor.kt | 2 +- .../cloudstream3/extractors/Pelisplus.kt | 0 .../extractors/PixelDrainExtractor.kt | 0 .../cloudstream3/extractors/PlayLtXyz.kt | 2 +- .../cloudstream3/extractors/PlayerVoxzer.kt | 0 .../cloudstream3/extractors/Rabbitstream.kt | 0 .../extractors/RapidVidExtractor.kt | 2 +- .../cloudstream3/extractors/SBPlay.kt | 0 .../cloudstream3/extractors/Sendvid.kt | 0 .../extractors/SibNetExtractor.kt | 2 +- .../cloudstream3/extractors/Solidfiles.kt | 0 .../cloudstream3/extractors/StreamSB.kt | 0 .../cloudstream3/extractors/StreamTape.kt | 0 .../extractors/StreamWishExtractor.kt | 0 .../cloudstream3/extractors/Streamhub.kt | 0 .../cloudstream3/extractors/Streamlare.kt | 0 .../cloudstream3/extractors/StreamoUpload.kt | 0 .../cloudstream3/extractors/Streamplay.kt | 0 .../cloudstream3/extractors/Supervideo.kt | 0 .../cloudstream3/extractors/TRsTXExtractor.kt | 2 +- .../cloudstream3/extractors/Tantifilm.kt | 0 .../extractors/TauVideoExtractor.kt | 2 +- .../cloudstream3/extractors/Tomatomatela.kt | 0 .../extractors/UpstreamExtractor.kt | 0 .../cloudstream3/extractors/Uqload.kt | 0 .../cloudstream3/extractors/Userload.kt | 0 .../cloudstream3/extractors/Userscloud.kt | 0 .../cloudstream3/extractors/Uservideo.kt | 0 .../cloudstream3/extractors/Vicloud.kt | 0 .../extractors/VidMoxyExtractor.kt | 2 +- .../extractors/VidSrcExtractor.kt | 0 .../cloudstream3/extractors/VidSrcTo.kt | 142 ++++----- .../extractors/VideoSeyredExtractor.kt | 2 +- .../cloudstream3/extractors/VideoVard.kt | 0 .../cloudstream3/extractors/Vidguard.kt | 4 +- .../extractors/VidhideExtractor.kt | 0 .../cloudstream3/extractors/Vidmoly.kt | 0 .../lagradost/cloudstream3/extractors/Vido.kt | 0 .../cloudstream3/extractors/Vidplay.kt | 0 .../cloudstream3/extractors/Vidstream.kt | 0 .../lagradost/cloudstream3/extractors/Voe.kt | 6 +- .../lagradost/cloudstream3/extractors/Vtbe.kt | 0 .../cloudstream3/extractors/WatchSB.kt | 0 .../cloudstream3/extractors/WcoStream.kt | 0 .../cloudstream3/extractors/Wibufile.kt | 0 .../cloudstream3/extractors/XStreamCdn.kt | 0 .../cloudstream3/extractors/YourUpload.kt | 0 .../extractors/YoutubeExtractor.kt | 0 .../cloudstream3/extractors/Zorofile.kt | 0 .../cloudstream3/extractors/Zplayer.kt | 0 .../extractors/helper/AesHelper.kt | 0 .../extractors/helper/AsianEmbedHelper.kt | 2 +- .../extractors/helper/GogoHelper.kt | 0 .../extractors/helper/NineAnimeHelper.kt | 0 .../extractors/helper/VstreamhubHelper.kt | 0 .../extractors/helper/WcoHelper.kt | 10 +- .../cloudstream3/network/WebViewResolver.kt | 28 ++ .../cloudstream3/syncproviders/SyncAPI.kt | 10 + .../lagradost/cloudstream3/utils/AppUtils.kt | 24 ++ .../cloudstream3/utils/ExtractorApi.kt | 28 +- .../cloudstream3/utils/M3u8Helper.kt | 0 .../cloudstream3/utils/UnshortenUrl.kt | 6 +- .../com/lagradost/api/ContextHelper.jvm.kt | 10 + .../network/WebViewResolver.jvm.kt | 35 +++ 181 files changed, 730 insertions(+), 608 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/syncproviders/{SyncAPI.kt => SyncApi.kt} (97%) rename app/src/main/java/com/lagradost/cloudstream3/utils/{AppUtils.kt => AppContextUtils.kt} (82%) create mode 100644 library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt rename app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt => library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt (90%) create mode 100644 library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/MainAPI.kt (86%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/AStreamHub.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Acefile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/AsianLoad.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Blogger.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/BullStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/ByteShare.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Cda.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Chillx.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Dailymotion.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/DoodExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/EPlay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Embedgram.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Evolaod.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Fastream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Filesim.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GMPlayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GenericM3U8.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Gofile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GuardareStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Hxfile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/JWPlayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Jawcloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Jeniusplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Krakenfiles.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Linkbox.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Maxstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mediafire.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Minoplres.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MixDrop.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Moviehab.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mp4Upload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MultiQuality.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mvidoo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Okrulink.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt (99%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Pelisplus.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt (99%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Rabbitstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/SBPlay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Sendvid.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Solidfiles.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamSB.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamTape.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamhub.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamlare.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamoUpload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Supervideo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Tantifilm.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Tomatomatela.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Uqload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Userload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Userscloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Uservideo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vicloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidSrcTo.kt (88%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VideoVard.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidguard.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidmoly.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vido.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Voe.kt (93%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vtbe.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/WatchSB.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/WcoStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Wibufile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/XStreamCdn.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/YourUpload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Zorofile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Zplayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt (76%) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/ExtractorApi.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/M3u8Helper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/UnshortenUrl.kt (96%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt create mode 100644 library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9c75a90d..ebefa0ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -263,6 +263,8 @@ tasks.register("copyJar") { // Merge the app classes and the library classes into classes.jar tasks.register("makeJar") { + // Duplicates cause hard to catch errors, better to fail at compile time. + duplicatesStrategy = DuplicatesStrategy.FAIL dependsOn(tasks.getByName("copyJar")) from( zipTree("build/app-classes/classes.jar"), diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 1680d698..598ff540 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -8,13 +8,14 @@ import android.content.Intent import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import com.lagradost.api.setContext import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -151,6 +152,7 @@ class AcraApplication : Application() { get() = _context?.get() private set(value) { _context = WeakReference(value) + setContext(WeakReference(value)) } fun getKeyClass(path: String, valueType: Class): T? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 82e985db..ba303fef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -36,7 +36,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.settings.Globals.updateTv -import com.lagradost.cloudstream3.utils.AppUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 21567e4d..a47e7685 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -56,9 +56,7 @@ import com.google.common.collect.Comparators.min import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.initAll -import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -121,16 +119,18 @@ import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions import com.lagradost.cloudstream3.utils.ApkInstaller -import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isLtr -import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isRtl -import com.lagradost.cloudstream3.utils.AppUtils.loadCache -import com.lagradost.cloudstream3.utils.AppUtils.loadRepository -import com.lagradost.cloudstream3.utils.AppUtils.loadResult -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.isLtr +import com.lagradost.cloudstream3.utils.AppContextUtils.isNetworkAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index c8c385cf..ce2fb3a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.network +import android.util.Base64 import android.util.Log import android.webkit.CookieManager import androidx.annotation.AnyThread 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 a5631500..6b2b75f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -14,7 +14,6 @@ import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.google.gson.Gson import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -33,6 +32,7 @@ import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt index 6ed7a447..4ef841f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt @@ -10,7 +10,7 @@ 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.AppContextUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt index e2bcd6e1..00c74dff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -10,13 +10,13 @@ import androidx.core.app.NotificationCompat import androidx.core.net.toUri import androidx.work.* import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index a14f8438..e86d73aa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,15 +3,22 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit abstract class AccountManager(private val defIndex: Int) : AuthAPI { companion object { - val malApi = MALApi(0) - val aniListApi = AniListApi(0) + val malApi = MALApi(0).also { api -> + LoadResponse.Companion.malIdPrefix = api.idPrefix + } + val aniListApi = AniListApi(0).also { api -> + LoadResponse.Companion.aniListIdPrefix = api.idPrefix + } + val simklApi = SimklApi(0).also { api -> + LoadResponse.Companion.simklIdPrefix = api.idPrefix + } val openSubtitlesApi = OpenSubtitlesApi(0) - val simklApi = SimklApi(0) val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt rename to app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt index 045fdc94..878e0cb3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt @@ -2,20 +2,10 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.ui.SyncWatchType -import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiText import me.xdrop.fuzzywuzzy.FuzzySearch -enum class SyncIdName { - Anilist, - MyAnimeList, - Trakt, - Imdb, - Simkl, - LocalList, -} - interface SyncAPI : OAuth2API { /** * Set this to true if the user updates something on the list like watch status or score diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 0551fe6c..8a82cf94 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 4249f949..24ef7136 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -20,7 +20,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import java.net.URL import java.security.SecureRandom diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 7d0514d1..6412ff1b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.AppUtils import okhttp3.Interceptor import okhttp3.Response 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 4385fa5e..27975d19 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 @@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mapper @@ -29,7 +31,6 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import okhttp3.Interceptor import okhttp3.Response import java.math.BigInteger @@ -184,32 +185,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } - /** - * Set of sync services simkl is compatible with. - * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id - */ - enum class SyncServices(val originalName: String) { - Simkl("simkl"), - Imdb("imdb"), - Tmdb("tmdb"), - AniList("anilist"), - Mal("mal"), - } - - /** - * The ID string is a way to keep a collection of services in one single ID using a map - * This adds a database service (like imdb) to the string and returns the new string. - */ - fun addIdToString(idString: String?, database: SyncServices, id: String?): String? { - if (id == null) return idString - return (readIdFromString(idString) + mapOf(database to id)).toJson() - } - - /** Read the id string to get all other ids */ - fun readIdFromString(idString: String?): Map { - return tryParseJson(idString) ?: return emptyMap() - } - fun getPosterUrl(poster: String): String { return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp" } @@ -361,13 +336,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("anilist") val anilist: String? = null, ) { companion object { - fun fromMap(map: Map): Ids { + fun fromMap(map: Map): Ids { return Ids( - simkl = map[SyncServices.Simkl]?.toIntOrNull(), - imdb = map[SyncServices.Imdb], - tmdb = map[SyncServices.Tmdb], - mal = map[SyncServices.Mal], - anilist = map[SyncServices.AniList] + simkl = map[SimklSyncServices.Simkl]?.toIntOrNull(), + imdb = map[SimklSyncServices.Imdb], + tmdb = map[SimklSyncServices.Tmdb], + mal = map[SimklSyncServices.Mal], + anilist = map[SimklSyncServices.AniList] ) } } @@ -749,13 +724,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("anilist") val anilist: String?, @JsonProperty("traktslug") val traktslug: String? ) { - fun matchesId(database: SyncServices, id: String): Boolean { + fun matchesId(database: SimklSyncServices, id: String): Boolean { return when (database) { - SyncServices.Simkl -> this.simkl == id.toIntOrNull() - SyncServices.AniList -> this.anilist == id - SyncServices.Mal -> this.mal == id - SyncServices.Tmdb -> this.tmdb == id - SyncServices.Imdb -> this.imdb == id + SimklSyncServices.Simkl -> this.simkl == id.toIntOrNull() + SimklSyncServices.AniList -> this.anilist == id + SimklSyncServices.Mal -> this.mal == id + SimklSyncServices.Tmdb -> this.tmdb == id + SimklSyncServices.Imdb -> this.imdb == id } } } @@ -916,7 +891,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ - suspend fun searchByIds(serviceMap: Map): Array? { + suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() return app.get( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 688363e9..6bafa975 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -23,13 +23,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.sortSubs import com.lagradost.cloudstream3.sortUrls import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 9ed58e2c..15e66b38 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding import com.lagradost.cloudstream3.network.WebViewResolver -import com.lagradost.cloudstream3.utils.AppUtils.loadRepository +import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository class WebviewFragment : Fragment() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt index 1db49e27..d2aca862 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt @@ -27,7 +27,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe 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.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index 1132416a..b4a16a66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -13,8 +13,9 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.UIHelper.setImage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 880d5f6c..c8c40e29 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -9,11 +9,11 @@ import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.ui.player.GeneratorPlayer -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index d5427cd3..82c5ffb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -39,7 +39,8 @@ import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DataStore import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe 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 12185cbf..82a92d80 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 @@ -25,8 +25,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.chip.Chip 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.CommonActivity.showToast import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding @@ -46,11 +44,13 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppUtils.ownHide -import com.lagradost.cloudstream3.utils.AppUtils.ownShow -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event 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 4b0360d7..916cb9ae 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 @@ -22,7 +22,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable +import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable class LoadClickCallback( val action: Int = 0, 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 52ec06db..2e98dd1f 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 @@ -16,7 +16,6 @@ import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup -import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList @@ -36,6 +35,7 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST +import com.lagradost.cloudstream3.ui.result.getId 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 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 a2c7583f..9e70d088 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 @@ -6,9 +6,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -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 @@ -36,8 +33,11 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching -import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.addProgramsToContinueWatching +import com.lagradost.cloudstream3.utils.AppContextUtils.filterHomePageListByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper 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 90e57ef4..7144de09 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 @@ -53,9 +53,9 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -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.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.reduceDragSensitivity import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt index b8feb656..b2de307f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -16,7 +16,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.UIHelper.toPx import kotlin.math.roundToInt @@ -26,7 +26,7 @@ class PageAdapter( private val resView: AutofitRecyclerView, val clickCallback: (SearchClickCallback) -> Unit ) : - AppUtils.DiffAdapter(items) { + AppContextUtils.DiffAdapter(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return LibraryItemViewHolder( 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 0865b220..9d838c97 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 @@ -44,8 +44,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.unixTimeMs 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.AppContextUtils +import com.lagradost.cloudstream3.utils.AppContextUtils.requestLocalAudioFocus import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper @@ -258,7 +258,7 @@ abstract class AbstractPlayerFragment( private fun requestAudioFocus() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity?.requestLocalAudioFocus(AppUtils.getFocusRequest()) + activity?.requestLocalAudioFocus(AppContextUtils.getFocusRequest()) } } 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 31adbc87..8e322f73 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 @@ -57,14 +57,13 @@ import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle -import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.AppContextUtils.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 import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File import java.lang.IllegalArgumentException diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 5585924e..3b242172 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -4,7 +4,6 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlin.math.max import kotlin.math.min 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 4279b542..92ef279d 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 @@ -8,10 +8,14 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.safefile.SafeFile import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri +const val DTAG = "PlayerActivity" + class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt index d8d2d537..8255360c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri class ExtractorLinkGenerator( private val links: List, 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 aa25157b..75a861c0 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 @@ -45,7 +45,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute 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 c77f9404..d827d31e 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 @@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index c5de1a1c..1e2cf4f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.ExtractorUri enum class LoadType { Unknown, 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 0e54e2cb..4bd5c769 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 @@ -6,7 +6,6 @@ import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri enum class PlayerEventType(val value: Int) { //Stop(-1), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 02f44eb9..89e3c8de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -1,13 +1,31 @@ package com.lagradost.cloudstream3.ui.player +import android.net.Uri +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.unshortenLinkSafe +data class ExtractorUri( + val uri: Uri, + val name: String, + + val basePath: String? = null, + val relativePath: String? = null, + val displayName: String? = null, + + val id: Int? = null, + val parentId: Int? = null, + val episode: Int? = null, + val season: Int? = null, + val headerName: String? = null, + val tvType: TvType? = null, +) + /** * Used to open the player more easily with the LinkGenerator **/ diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index a52ce160..e6de1266 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -5,7 +5,7 @@ import android.content.ContentUris import android.net.Uri import androidx.core.content.ContextCompat.getString import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index ee44567f..1ba5a29f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri import kotlinx.coroutines.Job import kotlinx.coroutines.launch 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 index fb600ef1..7c78ce63 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -9,14 +9,11 @@ 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.Globals import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 0a194785..90bd1ca7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri import kotlin.math.max import kotlin.math.min diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index fb60ccce..1e2c9f67 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -4,7 +4,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils data class SourcePriority( val data: T, @@ -13,7 +13,7 @@ data class SourcePriority( ) class PriorityAdapter(override val items: MutableList>) : - AppUtils.DiffAdapter>(items) { + AppContextUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index 8153d7a1..b587276f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding import com.lagradost.cloudstream3.ui.result.UiImage -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.UIHelper.setImage class ProfilesAdapter( @@ -21,7 +21,7 @@ class ProfilesAdapter( val usedProfile: Int, val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit, ) : - AppUtils.DiffAdapter( + AppContextUtils.DiffAdapter( items, comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile -> first.id == second.id 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 85e20d1c..12adc040 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 @@ -17,8 +17,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.bottomsheet.BottomSheetDialog -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList @@ -34,12 +32,13 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 7b743388..61188905 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -138,7 +138,7 @@ class ActorAdaptor( voiceActorImageHolder.isVisible = false voiceActorName.isVisible = false } else { - voiceActorName.text = actor.voiceActor.name + voiceActorName.text = actor.voiceActor?.name voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 62b1fdd1..0a1b777d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -21,7 +21,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper 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 1d3f5a08..c687eaa0 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 @@ -3,12 +3,12 @@ package com.lagradost.cloudstream3.ui.result import android.os.Bundle import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos 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 e185e75d..2f297098 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 @@ -29,7 +29,6 @@ import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse @@ -57,10 +56,11 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.loadCache -import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog 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 13621cda..a0207060 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 @@ -17,7 +17,6 @@ import androidx.fragment.app.Fragment 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 @@ -40,13 +39,13 @@ 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.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isRtl -import com.lagradost.cloudstream3.utils.AppUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.UIHelper 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 ac6527de..8e8dfe30 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 @@ -18,7 +18,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getId +import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -27,9 +27,9 @@ import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId -import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie +import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString import com.lagradost.cloudstream3.MainActivity.Companion.MPV import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE @@ -56,10 +56,11 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -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.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.isAppInstalled +import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork @@ -301,6 +302,23 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { ) } +data class ExtractorSubtitleLink( + val name: String, + override val url: String, + override val referer: String, + override val headers: Map = mapOf() +) : IDownloadableMinimum + +fun LoadResponse.getId(): Int { + // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) + ?: getLoadResponseIdFromUrl(url, apiName) +} + +private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { + return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "") + .hashCode() +} data class LinkProgress( val linksLoaded: Int, @@ -856,7 +874,7 @@ class ResultViewModel2 : ViewModel() { loadResponse: LoadResponse? = null, statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null ) { - val (response,currentId) = loadResponse?.let { load -> + val (response, currentId) = loadResponse?.let { load -> (load to load.getId()) } ?: ((currentResponse ?: return) to (currentId ?: return)) @@ -1140,12 +1158,16 @@ class ResultViewModel2 : ViewModel() { val message = if (duplicateEntries.size == 1) { val list = when (listType) { - LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes + 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, + context.getString( + R.string.duplicate_message_single, "${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}" ) } else { @@ -1170,9 +1192,11 @@ class ResultViewModel2 : ViewModel() { DialogInterface.BUTTON_POSITIVE -> { checkDuplicatesCallback.invoke(true, emptyList()) } + DialogInterface.BUTTON_NEGATIVE -> { checkDuplicatesCallback.invoke(false, emptyList()) } + DialogInterface.BUTTON_NEUTRAL -> { checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id }) } @@ -1189,17 +1213,17 @@ class ResultViewModel2 : ViewModel() { private fun getImdbIdFromSyncData(syncData: Map?): String? { return normalSafeApiCall { - SimklApi.readIdFromString( + readIdFromString( syncData?.get(AccountManager.simklApi.idPrefix) - )[SimklApi.Companion.SyncServices.Imdb] + )[SimklSyncServices.Imdb] } } private fun getTMDbIdFromSyncData(syncData: Map?): String? { return normalSafeApiCall { - SimklApi.readIdFromString( + readIdFromString( syncData?.get(AccountManager.simklApi.idPrefix) - )[SimklApi.Companion.SyncServices.Tmdb] + )[SimklSyncServices.Tmdb] } } @@ -1303,7 +1327,8 @@ class ResultViewModel2 : ViewModel() { postPopup( text, links.links.apmap { - val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" + val size = + it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") }) { callback.invoke(links to (it ?: return@postPopup)) @@ -1928,7 +1953,8 @@ class ResultViewModel2 : ViewModel() { .distinct().map { // this actually would be nice if we improved a bit as 3rd season == season 3 == III ect // right now it just removes the dubbed status - it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim() + it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)"""), "") + .trim() }, TrackerType.getTypes(this.type), this.year @@ -2276,7 +2302,7 @@ class ResultViewModel2 : ViewModel() { private suspend fun postSuccessful( loadResponse: LoadResponse, - mainId : Int, + mainId: Int, apiRepository: APIRepository, updateEpisodes: Boolean, updateFillers: Boolean, @@ -2292,7 +2318,11 @@ class ResultViewModel2 : ViewModel() { postEpisodes(loadResponse, mainId, updateFillers) } - private suspend fun postEpisodes(loadResponse: LoadResponse, mainId : Int, updateFillers: Boolean) { + private suspend fun postEpisodes( + loadResponse: LoadResponse, + mainId: Int, + updateFillers: Boolean + ) { _episodes.postValue(Resource.Loading()) if (updateFillers && loadResponse is AnimeLoadResponse) { @@ -2313,7 +2343,12 @@ class ResultViewModel2 : ViewModel() { ?: 0) val totalIndex = - i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) } + i.season?.let { season -> + loadResponse.getTotalEpisodeIndex( + episode, + season + ) + } if (!existingEpisodes.contains(id)) { existingEpisodes.add(id) @@ -2366,7 +2401,12 @@ class ResultViewModel2 : ViewModel() { loadResponse.seasonNames.getSeason(episode.season) val totalIndex = - episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) } + episode.season?.let { season -> + loadResponse.getTotalEpisodeIndex( + episodeIndex, + season + ) + } val ep = buildResultEpisode( @@ -2546,7 +2586,13 @@ class ResultViewModel2 : ViewModel() { ResumeProgress( progress = (viewPos.position / 1000).toInt(), maxProgress = (viewPos.duration / 1000).toInt(), - txt(R.string.resume_remaining, secondsToReadable(((viewPos.duration - viewPos.position) / 1_000).toInt(), "0 mins")) + txt( + R.string.resume_remaining, + secondsToReadable( + ((viewPos.duration - viewPos.position) / 1_000).toInt(), + "0 mins" + ) + ) ) } @@ -2672,17 +2718,26 @@ class ResultViewModel2 : ViewModel() { override var posterHeaders: Map? = null, override var backgroundPosterUrl: String? = null, override var contentRating: String? = null, - val id : Int?, + val id: Int?, ) : LoadResponse - fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe { + fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe { val url = searchResponse.url _page.postValue(Resource.Loading(url)) _episodes.postValue(Resource.Loading()) - val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi + val api = + APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull( + searchResponse.url + ) ?: APIRepository.noneApi val repo = APIRepository(api) - val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others, - posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply { + val response = LoadResponseFromSearch( + name = searchResponse.name, + url = searchResponse.url, + apiName = api.name, + type = searchResponse.type ?: TvType.Others, + posterUrl = searchResponse.posterUrl, + id = searchResponse.id + ).apply { if (searchResponse is SyncAPI.LibraryItem) { this.plot = searchResponse.plot this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating @@ -2701,7 +2756,8 @@ class ResultViewModel2 : ViewModel() { mainId = mainId, apiRepository = repo, updateEpisodes = false, - updateFillers = false) + updateFillers = false + ) } fun load( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index e0762cc5..70919943 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -10,7 +10,7 @@ import androidx.annotation.StringRes import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage sealed class UiText { 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 24e87d30..ef10fcee 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 @@ -24,11 +24,7 @@ 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.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.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AllLanguagesName @@ -58,9 +54,13 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -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.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index 66423982..ef1b8719 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -11,7 +11,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index d18c0197..f597132b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -19,7 +19,7 @@ import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.SubtitleHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 67a2a15b..15f8735f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -9,8 +9,6 @@ import android.view.inputmethod.EditorInfo import android.widget.TextView import androidx.annotation.UiThread import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity @@ -49,7 +47,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback @@ -64,9 +62,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.UIHelper.toPx import qrcode.QRCode -import java.io.ByteArrayOutputStream class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { companion object { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 7dc73a46..cfb46c39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -7,19 +7,17 @@ import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard -import com.lagradost.cloudstream3.utils.UIHelper.navigate class SettingsProviders : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 1364c376..1b487629 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -33,8 +33,8 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.addRepositoryDialog -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.addRepositoryDialog +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index cab029bb..909c30be 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -20,7 +20,7 @@ import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index 3bdcb251..c5319c37 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -8,7 +8,6 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R @@ -24,6 +23,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.appLanguages +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.toPx diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index 023ecb4c..bad58a0e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso @@ -23,7 +23,7 @@ import com.lagradost.cloudstream3.utils.TestingUtils import java.io.File class TestResultAdapter(override val items: MutableList>) : - AppUtils.DiffAdapter>(items) { + AppContextUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt index 26513f4a..eea495a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt @@ -13,7 +13,7 @@ import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.AppUtils.animateProgressTo +import com.lagradost.cloudstream3.utils.AppContextUtils.animateProgressTo class TestView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 59dcc402..c12e9eb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -11,11 +11,11 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentSetupProviderLanguagesBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt similarity index 82% rename from app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 626eca12..f0aae7bc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -51,6 +51,7 @@ import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.wrappers.Wrappers import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent @@ -60,9 +61,9 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment +import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.settings.Globals -import com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment import com.lagradost.cloudstream3.ui.settings.extensions.PluginsFragment import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -79,7 +80,7 @@ import java.io.* import java.net.URL import java.net.URLDecoder -object AppUtils { +object AppContextUtils { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { for (i in 0..maxViewTypeId) recycledViewPool.setMaxRecycledViews(i, maxPoolSize) @@ -371,6 +372,168 @@ object AppUtils { } } + fun sortSubs(subs: Set): List { + return subs.sortedBy { it.name } + } + + fun Context.getApiSettings(): HashSet { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + + val hashSet = HashSet() + val activeLangs = getApiProviderLangSettings() + val hasUniversal = activeLangs.contains(AllLanguagesName) + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } + .map { it.name }) + + /*val set = settingsManager.getStringSet( + this.getString(R.string.search_providers_list_key), + hashSet + )?.toHashSet() ?: hashSet + + val list = HashSet() + for (name in set) { + val api = getApiFromNameNull(name) ?: continue + if (activeLangs.contains(api.lang)) { + list.add(name) + } + }*/ + //if (list.isEmpty()) return hashSet + //return list + return hashSet + } + + fun Context.getApiDubstatusSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() + hashSet.addAll(DubStatus.values()) + val list = settingsManager.getStringSet( + this.getString(R.string.display_sub_key), + hashSet.map { it.name }.toMutableSet() + ) ?: return hashSet + + val names = DubStatus.values().map { it.name }.toHashSet() + //if(realSet.isEmpty()) return hashSet + + return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() + } + + fun Context.getApiProviderLangSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = hashSetOf(AllLanguagesName) // def is all languages +// hashSet.add("en") // def is only en + val list = settingsManager.getStringSet( + this.getString(R.string.provider_lang_key), + hashSet + ) + + if (list.isNullOrEmpty()) return hashSet + return list.toHashSet() + } + + fun Context.getApiTypeSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() + hashSet.addAll(TvType.values()) + val list = settingsManager.getStringSet( + this.getString(R.string.search_types_list_key), + hashSet.map { it.name }.toMutableSet() + ) + + if (list.isNullOrEmpty()) return hashSet + + val names = TvType.values().map { it.name }.toHashSet() + val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() + if (realSet.isEmpty()) return hashSet + + return realSet + } + + fun Context.updateHasTrailers() { + LoadResponse.isTrailersEnabled = getHasTrailers() + } + + private fun Context.getHasTrailers(): Boolean { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) + } + + fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List { + // We are getting the weirdest crash ever done: + // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType + // Trying fixing using classloader fuckery + val oldLoader = Thread.currentThread().contextClassLoader + Thread.currentThread().contextClassLoader = TvType::class.java.classLoader + + val default = TvType.values() + .sorted() + .filter { it != TvType.NSFW } + .map { it.ordinal } + + Thread.currentThread().contextClassLoader = oldLoader + + val defaultSet = default.map { it.toString() }.toSet() + val currentPrefMedia = try { + PreferenceManager.getDefaultSharedPreferences(this) + .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) + ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } + } catch (e: Throwable) { + null + } ?: default + val langs = this.getApiProviderLangSettings() + val hasUniversal = langs.contains(AllLanguagesName) + val allApis = synchronized(apis) { + apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } + return if (currentPrefMedia.isEmpty()) { + allApis + } else { + // Filter API depending on preferred media type + allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } } + } + } + + fun Context.filterSearchResultByFilmQuality(data: List): List { + // Filter results omitting entries with certain quality + if (data.isNotEmpty()) { + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) + ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) + ?.mapNotNull { entry -> + entry.toIntOrNull() ?: return@mapNotNull null + } ?: listOf() + if (filteredSearchQuality.isNotEmpty()) { + return data.filter { item -> + val searchQualVal = item.quality?.ordinal ?: -1 + //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") + !filteredSearchQuality.contains(searchQualVal) + } + } + } + return data + } + + fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { + // Filter results omitting entries with certain quality + if (data.list.isNotEmpty()) { + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) + ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) + ?.mapNotNull { entry -> + entry.toIntOrNull() ?: return@mapNotNull null + } ?: listOf() + if (filteredSearchQuality.isNotEmpty()) { + return HomePageList( + name = data.name, + isHorizontalImages = data.isHorizontalImages, + list = data.list.filter { item -> + val searchQualVal = item.quality?.ordinal ?: -1 + //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") + !filteredSearchQuality.contains(searchQualVal) + } + ) + } + } + return data + } + fun Activity.loadRepository(url: String) { ioSafe { val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe @@ -532,24 +695,6 @@ object AppUtils { return queryPairs } - /** Any object as json string */ - fun Any.toJson(): String { - if (this is String) return this - return mapper.writeValueAsString(this) - } - - inline fun parseJson(value: String): T { - return mapper.readValue(value) - } - - inline fun tryParseJson(value: String?): T? { - return try { - parseJson(value ?: return null) - } catch (_: Exception) { - null - } - } - /**| S1:E2 Hello World * | Episode 2. Hello world * | Hello World @@ -619,7 +764,7 @@ object AppUtils { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) Kitsu.isEnabled = settingsManager.getBoolean(this.getString(R.string.show_kitsu_posters_key), true) - }catch (t : Throwable) { + } catch (t: Throwable) { logError(t) } 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 04387d80..43124a53 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.utils import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty 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 @@ -18,6 +17,7 @@ import com.lagradost.cloudstream3.ui.WatchType 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.utils.AppContextUtils.filterProviderByPreferredMedia import kotlin.reflect.KClass import kotlin.reflect.KProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index d9a31b4e..89bb0031 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -24,7 +24,7 @@ import okio.sink import java.io.File import android.text.TextUtils import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader 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 322547f4..57b98dc2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import kotlinx.coroutines.delay diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 421b09e2..f3cbdaf1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -24,6 +24,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.IDownloadableMinimum import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType @@ -107,16 +108,6 @@ object VideoDownloadManager { Stop, } - interface IDownloadableMinimum { - val url: String - val referer: String - val headers: Map - } - - fun IDownloadableMinimum.getId(): Int { - return url.hashCode() - } - data class DownloadEpisodeMetadata( @JsonProperty("id") val id: Int, @JsonProperty("mainName") val mainName: String, diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 46da8e84..516e1ee9 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -1,4 +1,5 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { kotlin("multiplatform") @@ -12,6 +13,11 @@ kotlin { androidTarget() jvm() + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + sourceSets { commonMain.dependencies { implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib @@ -19,6 +25,9 @@ kotlin { ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API Level 25 or Less. */ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("me.xdrop:fuzzywuzzy:1.4.0") // Match extractors + implementation("org.mozilla:rhino:1.7.15") // run JavaScript + implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") } } } diff --git a/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt b/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt new file mode 100644 index 00000000..a8472fea --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt @@ -0,0 +1,20 @@ +package com.lagradost.api + +import android.content.Context +import java.lang.ref.WeakReference + +var ctx: WeakReference? = null + +/** + * Helper function for Android specific context. Not usable in JVM. + * Do not use this unless absolutely necessary. + */ +actual fun getContext(): Any? { + return ctx?.get() +} + +actual fun setContext(context: WeakReference) { + if (context.get() is Context) { + ctx = context as? WeakReference + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt similarity index 90% rename from app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt rename to library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt index 90872d94..0fbc5749 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt @@ -1,13 +1,12 @@ package com.lagradost.cloudstream3.network import android.annotation.SuppressLint +import android.content.Context import android.net.http.SslError import android.os.Handler import android.os.Looper import android.webkit.* -import com.lagradost.cloudstream3.AcraApplication -import com.lagradost.cloudstream3.AcraApplication.Companion.context -import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.api.getContext import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError @@ -33,40 +32,24 @@ import java.net.URI * @param scriptCallback will be called with the result from custom js * @param timeout close webview after timeout * */ -class WebViewResolver( +actual class WebViewResolver actual constructor( val interceptUrl: Regex, - val additionalUrls: List = emptyList(), - val userAgent: String? = USER_AGENT, - val useOkhttp: Boolean = true, - val script: String? = null, - val scriptCallback: ((String) -> Unit)? = null, - val timeout: Long = DEFAULT_TIMEOUT + val additionalUrls: List, + val userAgent: String?, + val useOkhttp: Boolean, + val script: String?, + val scriptCallback: ((String) -> Unit)?, + val timeout: Long ) : Interceptor { - constructor( - interceptUrl: Regex, - additionalUrls: List = emptyList(), - userAgent: String? = USER_AGENT, - useOkhttp: Boolean = true, - script: String? = null, - scriptCallback: ((String) -> Unit)? = null, - ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT) - - constructor( - interceptUrl: Regex, - additionalUrls: List = emptyList(), - userAgent: String? = USER_AGENT, - useOkhttp: Boolean = true - ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT) - - companion object { - private const val DEFAULT_TIMEOUT = 60_000L + actual companion object { var webViewUserAgent: String? = null + actual val DEFAULT_TIMEOUT = 60_000L @JvmName("getWebViewUserAgent1") fun getWebViewUserAgent(): String? { - return webViewUserAgent ?: context?.let { ctx -> + return webViewUserAgent ?: (getContext() as? Context)?.let { ctx -> runBlocking { mainWork { WebView(ctx).settings.userAgentString.also { userAgent -> @@ -137,7 +120,7 @@ class WebViewResolver( WebView.setWebContentsDebuggingEnabled(true) try { webView = WebView( - AcraApplication.context + (getContext() as? Context) ?: throw RuntimeException("No base context in WebViewResolver") ).apply { // Bare minimum to bypass captcha diff --git a/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt b/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt new file mode 100644 index 00000000..fb54e3ca --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt @@ -0,0 +1,16 @@ +package com.lagradost.api + +import java.lang.ref.WeakReference + +/** + * Set context for android specific code such as webview. + * Does nothing on JVM. + */ +expect fun setContext(context: WeakReference) +/** + * Helper function for Android specific context. + * Do not use this unless absolutely necessary. + * setContext() must be called before this is called. + * @return Context if on android, null if not. + */ +expect fun getContext(): Any? diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 91da2ed0..47ef5382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -1,34 +1,26 @@ package com.lagradost.cloudstream3 -import android.annotation.SuppressLint -import android.content.Context -import android.net.Uri -import android.util.Base64.encodeToString -import androidx.annotation.WorkerThread -import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.SyncIdName -import com.lagradost.cloudstream3.syncproviders.providers.SimklApi -import com.lagradost.cloudstream3.ui.player.SubtitleData -import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.nicehttp.RequestBodyTypes import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody +import java.net.URI import java.text.SimpleDateFormat import java.util.* +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.absoluteValue /** @@ -111,17 +103,6 @@ object APIHolder { return null } - private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { - return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "") - .hashCode() - } - - fun LoadResponse.getId(): Int { - // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked - return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) - ?: getLoadResponseIdFromUrl(url, apiName) - } - /** * Gets the website captcha token * discovered originally by https://github.com/ahmedgamal17 @@ -137,10 +118,9 @@ object APIHolder { // To get the key suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { try { - val uri = Uri.parse(url) - val domain = encodeToString( + val uri = URI.create(url) + val domain = base64Encode( (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), - 0 ).replace("\n", "").replace("=", ".") val vToken = @@ -275,165 +255,6 @@ object APIHolder { return app.post("https://graphql.anilist.co", requestBody = data) .parsedSafe() } - - - fun Context.getApiSettings(): HashSet { - //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - - val hashSet = HashSet() - val activeLangs = getApiProviderLangSettings() - val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } - .map { it.name }) - - /*val set = settingsManager.getStringSet( - this.getString(R.string.search_providers_list_key), - hashSet - )?.toHashSet() ?: hashSet - - val list = HashSet() - for (name in set) { - val api = getApiFromNameNull(name) ?: continue - if (activeLangs.contains(api.lang)) { - list.add(name) - } - }*/ - //if (list.isEmpty()) return hashSet - //return list - return hashSet - } - - fun Context.getApiDubstatusSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() - hashSet.addAll(DubStatus.values()) - val list = settingsManager.getStringSet( - this.getString(R.string.display_sub_key), - hashSet.map { it.name }.toMutableSet() - ) ?: return hashSet - - val names = DubStatus.values().map { it.name }.toHashSet() - //if(realSet.isEmpty()) return hashSet - - return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() - } - - fun Context.getApiProviderLangSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = hashSetOf(AllLanguagesName) // def is all languages -// hashSet.add("en") // def is only en - val list = settingsManager.getStringSet( - this.getString(R.string.provider_lang_key), - hashSet - ) - - if (list.isNullOrEmpty()) return hashSet - return list.toHashSet() - } - - fun Context.getApiTypeSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() - hashSet.addAll(TvType.values()) - val list = settingsManager.getStringSet( - this.getString(R.string.search_types_list_key), - hashSet.map { it.name }.toMutableSet() - ) - - if (list.isNullOrEmpty()) return hashSet - - val names = TvType.values().map { it.name }.toHashSet() - val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() - if (realSet.isEmpty()) return hashSet - - return realSet - } - - fun Context.updateHasTrailers() { - LoadResponse.isTrailersEnabled = getHasTrailers() - } - - private fun Context.getHasTrailers(): Boolean { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) - } - - fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List { - // We are getting the weirdest crash ever done: - // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType - // Trying fixing using classloader fuckery - val oldLoader = Thread.currentThread().contextClassLoader - Thread.currentThread().contextClassLoader = TvType::class.java.classLoader - - val default = TvType.values() - .sorted() - .filter { it != TvType.NSFW } - .map { it.ordinal } - - Thread.currentThread().contextClassLoader = oldLoader - - val defaultSet = default.map { it.toString() }.toSet() - val currentPrefMedia = try { - PreferenceManager.getDefaultSharedPreferences(this) - .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) - ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } - } catch (e: Throwable) { - null - } ?: default - val langs = this.getApiProviderLangSettings() - val hasUniversal = langs.contains(AllLanguagesName) - val allApis = synchronized(apis) { - apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } - } - return if (currentPrefMedia.isEmpty()) { - allApis - } else { - // Filter API depending on preferred media type - allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } } - } - } - - fun Context.filterSearchResultByFilmQuality(data: List): List { - // Filter results omitting entries with certain quality - if (data.isNotEmpty()) { - val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) - ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) - ?.mapNotNull { entry -> - entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf() - if (filteredSearchQuality.isNotEmpty()) { - return data.filter { item -> - val searchQualVal = item.quality?.ordinal ?: -1 - //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") - !filteredSearchQuality.contains(searchQualVal) - } - } - } - return data - } - - fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { - // Filter results omitting entries with certain quality - if (data.list.isNotEmpty()) { - val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) - ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) - ?.mapNotNull { entry -> - entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf() - if (filteredSearchQuality.isNotEmpty()) { - return HomePageList( - name = data.name, - isHorizontalImages = data.isHorizontalImages, - list = data.list.filter { item -> - val searchQualVal = item.quality?.ordinal ?: -1 - //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") - !filteredSearchQuality.contains(searchQualVal) - } - ) - } - } - return data - } } /* @@ -656,7 +477,7 @@ abstract class MainAPI { //emptyList() // open val mainPage = listOf(MainPageData("", "", false)) - @WorkerThread + // @WorkerThread open suspend fun getMainPage( page: Int, request: MainPageRequest, @@ -664,17 +485,17 @@ abstract class MainAPI { throw NotImplementedError() } - @WorkerThread + // @WorkerThread open suspend fun search(query: String): List? { throw NotImplementedError() } - @WorkerThread + // @WorkerThread open suspend fun quickSearch(query: String): List? { throw NotImplementedError() } - @WorkerThread + // @WorkerThread /** * Based on data from search() or getMainPage() it generates a LoadResponse, * basically opening the info page from a link. @@ -692,13 +513,13 @@ abstract class MainAPI { * This function might be updated to include exoplayer timestamps etc in the future * if the need arises. * */ - @WorkerThread + // @WorkerThread open suspend fun extractorVerifierJob(extractorData: String?) { throw NotImplementedError() } /**Callback is fired once a link is found, will return true if method is executed successfully*/ - @WorkerThread + // @WorkerThread open suspend fun loadLinks( data: String, isCasting: Boolean, @@ -723,27 +544,16 @@ abstract class MainAPI { } /** Might need a different implementation for desktop*/ -@SuppressLint("NewApi") fun base64Decode(string: String): String { return String(base64DecodeArray(string), Charsets.ISO_8859_1) } - -@SuppressLint("NewApi") +@OptIn(ExperimentalEncodingApi::class) fun base64DecodeArray(string: String): ByteArray { - return try { - android.util.Base64.decode(string, android.util.Base64.DEFAULT) - } catch (e: Exception) { - Base64.getDecoder().decode(string) - } + return Base64.decode(string) } - -@SuppressLint("NewApi") +@OptIn(ExperimentalEncodingApi::class) fun base64Encode(array: ByteArray): String { - return try { - String(android.util.Base64.encode(array, android.util.Base64.NO_WRAP), Charsets.ISO_8859_1) - } catch (e: Exception) { - String(Base64.getEncoder().encode(array)) - } + return Base64.encode(array) } fun MainAPI.fixUrlNull(url: String?): String? { @@ -779,10 +589,6 @@ fun sortUrls(urls: Set): List { return urls.sortedBy { t -> -t.quality } } -fun sortSubs(subs: Set): List { - return subs.sortedBy { it.name } -} - fun capitalizeString(str: String): String { return capitalizeStringNullable(str) ?: str } @@ -1204,11 +1010,25 @@ interface LoadResponse { var contentRating: String? companion object { - private val malIdPrefix = malApi.idPrefix - private val aniListIdPrefix = aniListApi.idPrefix - private val simklIdPrefix = simklApi.idPrefix + var malIdPrefix = "" //malApi.idPrefix + var aniListIdPrefix = "" //aniListApi.idPrefix + var simklIdPrefix = "" //simklApi.idPrefix var isTrailersEnabled = true + /** + * The ID string is a way to keep a collection of services in one single ID using a map + * This adds a database service (like imdb) to the string and returns the new string. + */ + fun addIdToString(idString: String?, database: SimklSyncServices, id: String?): String? { + if (id == null) return idString + return (readIdFromString(idString) + mapOf(database to id)).toJson() + } + + /** Read the id string to get all other ids */ + fun readIdFromString(idString: String?): Map { + return tryParseJson(idString) ?: return emptyMap() + } + fun LoadResponse.isMovie(): Boolean { return this.type.isMovieType() || this is MovieLoadResponse } @@ -1232,12 +1052,12 @@ interface LoadResponse { * Internal helper function to add simkl ids from other databases. */ private fun LoadResponse.addSimklId( - database: SimklApi.Companion.SyncServices, + database: SimklSyncServices, id: String? ) { normalSafeApiCall { this.syncData[simklIdPrefix] = - SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString()) + addIdToString(this.syncData[simklIdPrefix], database, id.toString()) ?: return@normalSafeApiCall } } @@ -1257,30 +1077,28 @@ interface LoadResponse { fun LoadResponse.getImdbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix]) - ?.get(SimklApi.Companion.SyncServices.Imdb) + readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Imdb] } } fun LoadResponse.getTMDbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix]) - ?.get(SimklApi.Companion.SyncServices.Tmdb) + readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Tmdb] } } fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() - this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) + this.addSimklId(SimklSyncServices.Mal, id.toString()) } fun LoadResponse.addAniListId(id: Int?) { this.syncData[aniListIdPrefix] = (id ?: return).toString() - this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString()) + this.addSimklId(SimklSyncServices.AniList, id.toString()) } fun LoadResponse.addSimklId(id: Int?) { - this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString()) + this.addSimklId(SimklSyncServices.Simkl, id.toString()) } fun LoadResponse.addImdbUrl(url: String?) { @@ -1362,7 +1180,7 @@ interface LoadResponse { fun LoadResponse.addImdbId(id: String?) { // TODO add imdb sync - this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id) + this.addSimklId(SimklSyncServices.Imdb, id) } fun LoadResponse.addTrackId(id: String?) { @@ -1375,7 +1193,7 @@ interface LoadResponse { fun LoadResponse.addTMDbId(id: String?) { // TODO add TMDb sync - this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id) + this.addSimklId(SimklSyncServices.Tmdb, id) } fun LoadResponse.addRating(text: String?) { @@ -1466,7 +1284,7 @@ data class NextAiring( constructor( episode: Int, unixTime: Long, - ) : this ( + ) : this( episode, unixTime, null @@ -1929,6 +1747,28 @@ fun MainAPI.newEpisode( return builder } +interface IDownloadableMinimum { + val url: String + val referer: String + val headers: Map +} + +fun IDownloadableMinimum.getId(): Int { + return url.hashCode() +} + +/** + * Set of sync services simkl is compatible with. + * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id + */ +enum class SimklSyncServices(val originalName: String) { + Simkl("simkl"), + Imdb("imdb"), + Tmdb("tmdb"), + AniList("anilist"), + Mal("mal"), +} + data class TvSeriesLoadResponse( override var name: String, override var url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt index b0051ba7..23f8dcf4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Acefile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Acefile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt index b7f84af1..27a5c52a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EPlay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EPlay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 03586386..1f70ce61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.extractors.helper.AesHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index 14333d35..8318c3fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index 766c7762..ce742e97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mediafire.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mediafire.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mediafire.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mediafire.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 46f6ad0f..6db0830c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt similarity index 99% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index b57449bf..0a005036 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt similarity index 99% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt index 2b286abb..a4dc694e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index a0d830cf..607d2d78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt index a8bcee31..ebd57f9c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index 645d7c0e..de5ca9a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 2478edc1..157374a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index b963fe56..e57772ce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt similarity index 88% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 2655670d..73857fb3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -1,70 +1,72 @@ -package com.lagradost.cloudstream3.extractors - -import android.util.Base64 -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.amap -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import java.net.URLDecoder -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec - -class VidSrcTo : ExtractorApi() { - override val name = "VidSrcTo" - override val mainUrl = "https://vidsrc.to" - override val requiresReferer = true - - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return - val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return - if (res.status != 200) return - res.result?.amap { source -> - try { - val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap - val finalUrl = DecryptUrl(embedRes.result.encUrl) - if(finalUrl.equals(embedRes.result.encUrl)) return@amap - when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) - "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) - } - } catch (e: Exception) { - logError(e) - } - } - } - - private fun DecryptUrl(encUrl: String): String { - var data = encUrl.toByteArray() - data = Base64.decode(data, Base64.URL_SAFE) - val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") - val cipher = Cipher.getInstance("RC4") - cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) - data = cipher.doFinal(data) - return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") - } - - data class VidsrctoEpisodeSources( - @JsonProperty("status") val status: Int, - @JsonProperty("result") val result: List? - ) - - data class VidsrctoResult( - @JsonProperty("id") val id: String, - @JsonProperty("title") val title: String - ) - - data class VidsrctoEmbedSource( - @JsonProperty("status") val status: Int, - @JsonProperty("result") val result: VidsrctoUrl - ) - - data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) -} +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + try { + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } catch (e: Exception) { + logError(e) + } + } + } + + @OptIn(ExperimentalEncodingApi::class) + private fun DecryptUrl(encUrl: String): String { + val data = Base64.UrlSafe.decode(encUrl) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + val finalData = cipher.doFinal(data) + return URLDecoder.decode(finalData.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index 2439b8ad..1161ff66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt index 230a9e1a..c48b683c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.AppUtils @@ -87,7 +87,7 @@ open class Vidguardto : ExtractorApi() { } Log.d("runJS", "Result: $result") } catch (e: Exception) { - Log.e("runJS", "Error executing JavaScript", e) + Log.e("runJS", "Error executing JavaScript: ${e.message}") } finally { Context.exit() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt similarity index 93% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt index 67fd7eea..1d7dee7c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.extractors -import android.util.Base64 import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink @@ -58,12 +58,12 @@ open class Voe : ExtractorApi() { videoLinks.add( when { linkRegex.matches(link) -> link - else -> String(Base64.decode(link, Base64.DEFAULT)) + else -> base64Decode(link) } ) } else { val link2 = base64Regex.find(script)?.value ?: return - val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val decoded = base64Decode(link2) val videoLinkDTO = AppUtils.parseJson(decoded) videoLinkDTO.let { videoLinks.add(it.toString()) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt index 0b401c06..bd42424f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors.helper -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt similarity index 76% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt index 768fa1f6..35aec2b1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt @@ -1,8 +1,6 @@ package com.lagradost.cloudstream3.extractors.helper import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.app class WcoHelper { @@ -30,9 +28,7 @@ class WcoHelper { private suspend fun getKeys() { keys = keys ?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json") - .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey( - BACKUP_KEY_DATA - ) + .parsedSafe() } suspend fun getWcoKey(): ExternalKeys? { @@ -43,9 +39,7 @@ class WcoHelper { private suspend fun getNewKeys() { newKeys = newKeys ?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json") - .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey( - BACKUP_KEY_DATA - ) + .parsedSafe() } suspend fun getNewWcoKey(): NewExternalKeys? { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt new file mode 100644 index 00000000..8baf2f31 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream3.network + +import com.lagradost.cloudstream3.USER_AGENT +import okhttp3.Interceptor + +/** + * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) + * @param interceptUrl will stop the WebView when reaching this url. + * @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex. + * @param userAgent if null then will use the default user agent + * @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare. + * @param script pass custom js to execute + * @param scriptCallback will be called with the result from custom js + * @param timeout close webview after timeout + * */ +expect class WebViewResolver( + interceptUrl: Regex, + additionalUrls: List = emptyList(), + userAgent: String? = USER_AGENT, + useOkhttp: Boolean = true, + script: String? = null, + scriptCallback: ((String) -> Unit)? = null, + timeout: Long = DEFAULT_TIMEOUT +) : Interceptor { + companion object { + val DEFAULT_TIMEOUT: Long + } +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt new file mode 100644 index 00000000..676ac6fe --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -0,0 +1,10 @@ +package com.lagradost.cloudstream3.syncproviders + +enum class SyncIdName { + Anilist, + MyAnimeList, + Trakt, + Imdb, + Simkl, + LocalList, +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt new file mode 100644 index 00000000..374751a8 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -0,0 +1,24 @@ +package com.lagradost.cloudstream3.utils + +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.mapper + +object AppUtils { + /** Any object as json string */ + fun Any.toJson(): String { + if (this is String) return this + return mapper.writeValueAsString(this) + } + + inline fun parseJson(value: String): T { + return mapper.readValue(value) + } + + inline fun tryParseJson(value: String?): T? { + return try { + parseJson(value ?: return null) + } catch (_: Exception) { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index ce6e5ecc..566e29f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,9 +1,8 @@ package com.lagradost.cloudstream3.utils -import android.net.Uri import com.fasterxml.jackson.annotation.JsonIgnore +import com.lagradost.cloudstream3.IDownloadableMinimum import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.extractors.AStreamHub @@ -431,7 +430,7 @@ open class ExtractorLink constructor( /** Used for getExtractorVerifierJob() */ open val extractorData: String? = null, open val type: ExtractorLinkType, -) : VideoDownloadManager.IDownloadableMinimum { +) : IDownloadableMinimum { val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 val isDash: Boolean get() = type == ExtractorLinkType.DASH @@ -530,29 +529,6 @@ open class ExtractorLink constructor( } } -data class ExtractorUri( - val uri: Uri, - val name: String, - - val basePath: String? = null, - val relativePath: String? = null, - val displayName: String? = null, - - val id: Int? = null, - val parentId: Int? = null, - val episode: Int? = null, - val season: Int? = null, - val headerName: String? = null, - val tvType: TvType? = null, -) - -data class ExtractorSubtitleLink( - val name: String, - override val url: String, - override val referer: String, - override val headers: Map = mapOf() -) : VideoDownloadManager.IDownloadableMinimum - /** * Removes https:// and www. * To match urls regardless of schema, perhaps Uri() can be used? diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt similarity index 96% rename from app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt index 46b232f6..b13e88e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.utils -import android.util.Base64 import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Decode import com.lagradost.nicehttp.NiceResponse @@ -91,13 +90,12 @@ object ShortLink { } val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray() var decodedUri = - Base64.decode(encodedbytearray, Base64.DEFAULT).decodeToString().dropLast(16) + base64Decode(encodedbytearray.toString()).dropLast(16) .drop(16) if (Regex("""go\.php\?u=""").find(decodedUri) != null) { decodedUri = - Base64.decode(decodedUri.replace(Regex("""(.*?)u="""), ""), Base64.DEFAULT) - .decodeToString() + base64Decode(decodedUri.replace(Regex("""(.*?)u="""), "")) } return decodedUri diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt new file mode 100644 index 00000000..a30810b8 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt @@ -0,0 +1,10 @@ +package com.lagradost.api + +import java.lang.ref.WeakReference + +actual fun getContext(): Any? { + return null +} + +actual fun setContext(context: WeakReference) { +} \ No newline at end of file diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt new file mode 100644 index 00000000..6b99ef3b --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt @@ -0,0 +1,35 @@ +package com.lagradost.cloudstream3.network + +import okhttp3.Interceptor +import okhttp3.Response + +/** + * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) + * @param interceptUrl will stop the WebView when reaching this url. + * @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex. + * @param userAgent if null then will use the default user agent + * @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare. + * @param script pass custom js to execute + * @param scriptCallback will be called with the result from custom js + * @param timeout close webview after timeout + * */ +actual class WebViewResolver actual constructor( + interceptUrl: Regex, + additionalUrls: List, + userAgent: String?, + useOkhttp: Boolean, + script: String?, + scriptCallback: ((String) -> Unit)?, + timeout: Long +) : + Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + return chain.proceed(request) + } + + actual companion object { + actual val DEFAULT_TIMEOUT = 60_000L + } +} From e5c9e96c8347cf31cc7ee25e2490cc70046167c3 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:33:21 +0200 Subject: [PATCH 108/157] fix filesystem --- .../commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt | 5 +++++ .../commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 47ef5382..aa08cb59 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -29,6 +29,11 @@ import kotlin.math.absoluteValue **/ const val AllLanguagesName = "universal" +const val USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + +class ErrorLoadingException(message: String? = null) : Exception(message) + //val baseHeader = mapOf("User-Agent" to USER_AGENT) val mapper = JsonMapper.builder().addModule(kotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt deleted file mode 100644 index 160ff098..00000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.lagradost.cloudstream3 - -const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - -class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file From c1b5f5c128859158a5ef022d0f16eb129b963651 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:51:07 -0600 Subject: [PATCH 109/157] Fix download button display bug in adapter (#1175) --- .../ui/download/DownloadAdapter.kt | 22 ++++++-- .../ui/download/DownloadFragment.kt | 3 +- .../ui/download/button/PieFetchButton.kt | 50 +++++++++---------- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index b4a16a66..9a026334 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -13,9 +14,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull -import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -135,8 +135,15 @@ class DownloadAdapter( downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } downloadHeaderInfo.text = formattedSizeString - } else downloadButton.doSetProgress = true + } else { + downloadButton.doSetProgress = true + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + } downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) downloadButton.isVisible = true @@ -197,8 +204,15 @@ class DownloadAdapter( downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) - } else downloadButton.doSetProgress = true + } else { + downloadButton.doSetProgress = true + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + } downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) downloadButton.isVisible = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 82c5ffb8..23d546e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -40,7 +40,6 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult -import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DataStore import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -273,4 +272,4 @@ class DownloadFragment : Fragment() { val selectedVideoUri = result?.data?.data ?: return@registerForActivityResult playUri(activity ?: return@registerForActivityResult, selectedVideoUri) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index a6dc5c56..abc159d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.ui.download.button import android.content.Context -import android.graphics.drawable.Drawable import android.os.Looper import android.util.AttributeSet import android.util.Log @@ -45,6 +44,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : private var iconPaused: Int = 0 private var hideWhenIcon: Boolean = true + var progressDrawable: Int = 0 + var overrideLayout: Int? = null companion object { @@ -115,10 +116,10 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : R.styleable.PieFetchButton_download_icon_complete, R.drawable.download_icon_done ) iconPaused = getResourceId( - R.styleable.PieFetchButton_download_icon_paused, 0//R.drawable.download_icon_pause + R.styleable.PieFetchButton_download_icon_paused, 0 // R.drawable.download_icon_pause ) iconActive = getResourceId( - R.styleable.PieFetchButton_download_icon_active, 0 //R.drawable.download_icon_load + R.styleable.PieFetchButton_download_icon_active, 0 // R.drawable.download_icon_load ) iconWaiting = getResourceId( R.styleable.PieFetchButton_download_icon_waiting, 0 @@ -129,7 +130,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : val fillIndex = getInt(R.styleable.PieFetchButton_download_fill, 0) - val progressDrawable = getResourceId( + progressDrawable = getResourceId( R.styleable.PieFetchButton_download_fill_override, fillArray[fillIndex] ) @@ -170,7 +171,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : if (isZeroBytes) { removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) - //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) + // callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { val list = arrayListOf( Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), @@ -197,7 +198,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : list ) { callback(DownloadClickEvent(itemId, card)) - //callback.invoke(DownloadClickEvent(itemId, data)) + // callback.invoke(DownloadClickEvent(itemId, data)) } } } @@ -205,7 +206,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : view.setOnLongClickListener { callback(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, card)) - //clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) + // clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) return@setOnLongClickListener true } } @@ -218,7 +219,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : setDefaultClickListener(this, textView, card, callback) } - /*open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List) { + /* open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List) { this.setOnClickListener { when (this.currentStatus) { null -> { @@ -244,7 +245,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : else -> {} } } - }*/ + } */ @MainThread private fun setStatusInternal(status : DownloadStatusTell?) { @@ -262,7 +263,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : progressBarBackground.background = ContextCompat.getDrawable(context, progressDrawable) - val drawable = getDrawableFromStatus(status) + val drawable = + getDrawableFromStatus(status)?.let { ContextCompat.getDrawable(this.context, it) } statusView.setImageDrawable(drawable) val isDrawable = drawable != null @@ -280,12 +282,12 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : override fun setStatus(status: DownloadStatusTell?) { currentStatus = status - // runs on the main thread, but also instant if it already is + // Runs on the main thread, but also instant if it already is if (Looper.myLooper() == Looper.getMainLooper()) { try { setStatusInternal(status) } catch (t : Throwable) { - logError(t) // just in case setStatusInternal throws because thread + logError(t) // Just in case setStatusInternal throws because thread progressBarBackground.post { setStatusInternal(status) } @@ -325,19 +327,13 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } } - open fun getDrawableFromStatus(status: DownloadStatusTell?): Drawable? { - val drawableInt = when (status) { - DownloadStatusTell.IsPaused -> iconPaused - DownloadStatusTell.IsPending -> iconWaiting - DownloadStatusTell.IsDownloading -> iconActive - DownloadStatusTell.IsFailed -> iconError - DownloadStatusTell.IsDone -> iconComplete - DownloadStatusTell.IsStopped -> iconRemoved - null -> iconInit - } - if (drawableInt == 0) { - return null - } - return ContextCompat.getDrawable(this.context, drawableInt) - } + open fun getDrawableFromStatus(status: DownloadStatusTell?): Int? = when (status) { + DownloadStatusTell.IsPaused -> iconPaused + DownloadStatusTell.IsPending -> iconWaiting + DownloadStatusTell.IsDownloading -> iconActive + DownloadStatusTell.IsFailed -> iconError + DownloadStatusTell.IsDone -> iconComplete + DownloadStatusTell.IsStopped -> iconRemoved + else -> iconInit + }.takeIf { it != 0 } } \ No newline at end of file From e1d4a46309f8a979327e5b5486f2f8753f0a0639 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:26:44 +0200 Subject: [PATCH 110/157] bugfix on lib startup --- .../lagradost/cloudstream3/MainActivity.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a47e7685..59f499c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1112,23 +1112,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa MainAPI.settingsForProvider = settingsForProvider - // Change library icon with logo of current api in sync - libraryViewModel = ViewModelProvider(this)[LibraryViewModel::class.java] - libraryViewModel?.currentApiName?.observe(this) { - val syncAPI = libraryViewModel?.currentSyncApi - Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}") - val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) { - R.drawable.library_icon - } else { - syncAPI?.icon ?: R.drawable.library_icon - } - - binding?.apply { - navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon) - navView.menu.findItem(R.id.navigation_library)?.setIcon(icon) - } - } - loadThemes(this) updateLocale() super.onCreate(savedInstanceState) @@ -1538,6 +1521,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa logError(e) } } + + // we need to run this after we init all apis, otherwise currentSyncApi will fuck itself + this@MainActivity.runOnUiThread { + // Change library icon with logo of current api in sync + libraryViewModel = ViewModelProvider(this@MainActivity)[LibraryViewModel::class.java] + libraryViewModel?.currentApiName?.observe(this@MainActivity) { + val syncAPI = libraryViewModel?.currentSyncApi + Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}") + val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) { + R.drawable.library_icon + } else { + syncAPI?.icon ?: R.drawable.library_icon + } + + binding?.apply { + navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon) + navView.menu.findItem(R.id.navigation_library)?.setIcon(icon) + } + } + } } SearchResultBuilder.updateCache(this) From 699a6979a5d6a924859d5dff122de34389a100a7 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:04:32 +0300 Subject: [PATCH 111/157] feat(TV UI): Fix clone site focus (#1179) --- app/src/main/res/layout/add_remove_sites.xml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/layout/add_remove_sites.xml b/app/src/main/res/layout/add_remove_sites.xml index 9ef6ad6a..653f607f 100644 --- a/app/src/main/res/layout/add_remove_sites.xml +++ b/app/src/main/res/layout/add_remove_sites.xml @@ -1,19 +1,21 @@ + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/add_site" + android:text="@string/add_site_pref" + android:focusable="true" + style="@style/SettingsItem"> + android:id="@+id/remove_site" + android:text="@string/remove_site_pref" + android:focusable="true" + style="@style/SettingsItem" /> \ No newline at end of file From 9b1ac5fc28774585f207acd0a5444cc9d09933b6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:05:32 +0300 Subject: [PATCH 112/157] feat(Trakt): Skip specials season for next airing (#1181) --- .../com/lagradost/cloudstream3/metaproviders/TraktProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 736e05f2..7c375e0a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -238,7 +238,7 @@ open class TraktProvider : MainAPI() { description = episode.overview, ).apply { this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") - if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { + if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) { nextAir = NextAiring( episode = this.episode!!, unixTime = this.date!!.div(1000L), From 145c42f1c8bdbd53a18733786778be6ff9f77d2b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:10:58 +0300 Subject: [PATCH 113/157] feat(UI): Use same Episode holder size (#1180) --- .../cloudstream3/ui/result/EpisodeAdapter.kt | 7 +++---- app/src/main/res/layout/result_episode.xml | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 0a1b777d..ed5e51f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -57,8 +57,7 @@ const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 const val ACTION_FCAST = 19 -const val TV_EP_SIZE_LARGE = 400 -const val TV_EP_SIZE_SMALL = 300 +const val TV_EP_SIZE = 400 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -181,7 +180,7 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { localCard = card val setWidth = - if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE.toPx else ViewGroup.LayoutParams.MATCH_PARENT binding.episodeLinHolder.layoutParams.width = setWidth binding.episodeHolderLarge.layoutParams.width = setWidth @@ -336,7 +335,7 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { binding.episodeHolder.layoutParams.apply { width = - if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE.toPx else ViewGroup.LayoutParams.MATCH_PARENT } binding.apply { diff --git a/app/src/main/res/layout/result_episode.xml b/app/src/main/res/layout/result_episode.xml index b56cdb1d..36d60bd6 100644 --- a/app/src/main/res/layout/result_episode.xml +++ b/app/src/main/res/layout/result_episode.xml @@ -90,14 +90,15 @@ android:textColor="?attr/textColor" tools:text="Episode 1" /> - - + + + \ No newline at end of file From e86c926c30d565841d0701adac937d48dc9a8d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 8 Jul 2024 23:59:02 +0300 Subject: [PATCH 114/157] Extractor: added Pichive & Sobreatsesuyp (#1184) --- .../extractors/HotlingerExtractor.kt | 5 ++ .../extractors/SobreatsesuypExtractor.kt | 56 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 4 ++ 3 files changed, 65 insertions(+) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index db721108..11f8ccaf 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -22,6 +22,11 @@ class FourPlayRu : ContentX() { override var mainUrl = "https://four.playru.net" } +class Pichive : ContentX() { + override var name = "Pichive" + override var mainUrl = "https://pichive.online" +} + class FourPichive : ContentX() { override var name = "FourPichive" override var mainUrl = "https://four.pichive.online" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt new file mode 100644 index 00000000..91b60dac --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt @@ -0,0 +1,56 @@ +// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır. + +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import com.fasterxml.jackson.annotation.JsonProperty + +open class Sobreatsesuyp : ExtractorApi() { + override val name = "Sobreatsesuyp" + override val mainUrl = "https://sobreatsesuyp.com" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { + val extRef = referer ?: "" + + val videoReq = app.get(url, referer = extRef).text + + val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val postLink = "${mainUrl}/" + file.replace("\\", "") + val rawList = app.post(postLink, referer = extRef).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") + + val postJson: List = rawList.drop(1).map { item -> + val mapItem = item as Map<*, *> + SobreatsesuypVideoData( + title = mapItem["title"] as? String, + file = mapItem["file"] as? String + ) + } + Log.d("Kekik_${this.name}", "postJson » ${postJson}") + + for (item in postJson) { + if (item.file == null || item.title == null) continue + + val fileUrl = "${mainUrl}/playlist/${item.file.substring(1)}.txt" + val videoData = app.post(fileUrl, referer = extRef).text + + callback.invoke( + ExtractorLink( + source = this.name, + name = "${this.name} - ${item.title}", + url = videoData, + referer = extRef, + quality = Qualities.Unknown.value, + type = INFER_TYPE + ) + ) + } + } + + data class SobreatsesuypVideoData( + @JsonProperty("title") val title: String? = null, + @JsonProperty("file") val file: String? = null + ) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 566e29f0..0df73a0e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -115,6 +115,7 @@ import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.PlayRu import com.lagradost.cloudstream3.extractors.FourPlayRu +import com.lagradost.cloudstream3.extractors.Pichive import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem @@ -124,6 +125,7 @@ import com.lagradost.cloudstream3.extractors.HDStreamAble import com.lagradost.cloudstream3.extractors.RapidVid import com.lagradost.cloudstream3.extractors.TRsTX import com.lagradost.cloudstream3.extractors.VidMoxy +import com.lagradost.cloudstream3.extractors.Sobreatsesuyp import com.lagradost.cloudstream3.extractors.PixelDrain import com.lagradost.cloudstream3.extractors.MailRu import com.lagradost.cloudstream3.extractors.Mediafire @@ -734,6 +736,7 @@ val extractorApis: MutableList = arrayListOf( FourCX(), PlayRu(), FourPlayRu(), + Pichive(), FourPichive(), HDMomPlayer(), HDPlayerSystem(), @@ -743,6 +746,7 @@ val extractorApis: MutableList = arrayListOf( RapidVid(), TRsTX(), VidMoxy(), + Sobreatsesuyp(), PixelDrain(), MailRu(), From 8be8e5474647e5aeb31cb1026fe2e350dbf3c139 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:17:25 +0200 Subject: [PATCH 115/157] Fixed log --- .../cloudstream3/extractors/SobreatsesuypExtractor.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt index 91b60dac..c90b22f4 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -28,15 +27,13 @@ open class Sobreatsesuyp : ExtractorApi() { file = mapItem["file"] as? String ) } - Log.d("Kekik_${this.name}", "postJson » ${postJson}") for (item in postJson) { if (item.file == null || item.title == null) continue - val fileUrl = "${mainUrl}/playlist/${item.file.substring(1)}.txt" - val videoData = app.post(fileUrl, referer = extRef).text + val videoData = app.post("${mainUrl}/playlist/${item.file.substring(1)}.txt", referer = extRef).text - callback.invoke( + callback.invoke( ExtractorLink( source = this.name, name = "${this.name} - ${item.title}", From febb843424e0331b63b2e26ad796e797f7267ccc Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Mon, 15 Jul 2024 08:06:20 -0700 Subject: [PATCH 116/157] Fix VidSrcTo extractor (#1198) --- .../cloudstream3/extractors/Chillx.kt | 29 +++++++++---------- .../cloudstream3/extractors/VidSrcTo.kt | 14 ++++++++- .../cloudstream3/extractors/Vidplay.kt | 27 +++++++++++++---- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt index 26567c7a..dd22efb2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -6,6 +6,7 @@ import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import kotlin.run class Moviesapi : Chillx() { override val name = "Moviesapi" @@ -28,17 +29,22 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { + private val keySource = "https://rowdy-avocado.github.io/multi-keys/" + private var key: String? = null - suspend fun fetchKey(): String { - return if (key != null) { - key!! - } else { - val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") - key = fetch - key!! - } + private suspend fun fetchKey(): String { + return key + ?: run { + val res = + app.get(keySource).parsedSafe() + ?: throw ErrorLoadingException("Unable to get keys") + key = res.keys.get(0) + res.keys.get(0) + } } + + private data class KeysData(@JsonProperty("chillx") val keys: List) } @Suppress("NAME_SHADOWING") @@ -97,11 +103,4 @@ open class Chillx : ExtractorApi() { it.groupValues[1].toInt(16).toChar().toString() } } - - - - data class Keys( - @JsonProperty("chillx") val key: List - ) - } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 73857fb3..578f5fb9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -26,7 +26,13 @@ class VidSrcTo : ExtractorApi() { callback: (ExtractorLink) -> Unit ) { val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return - val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + val subtitlesLink = "$mainUrl/ajax/embed/episode/$mediaId/subtitles" + val subRes = app.get(subtitlesLink).parsedSafe>() + subRes?.forEach { + if (it.kind.equals("captions")) subtitleCallback.invoke(SubtitleFile(it.label, it.file)) + } + val sourcesLink = "$mainUrl/ajax/embed/episode/$mediaId/sources" + val res = app.get(sourcesLink).parsedSafe() ?: return if (res.status != 200) return res.result?.amap { source -> try { @@ -68,5 +74,11 @@ class VidSrcTo : ExtractorApi() { @JsonProperty("result") val result: VidsrctoUrl ) + data class VidsrctoSubtitles( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + @JsonProperty("kind") val kind: String + ) + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt index cb9eaf1e..6202800f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Encode @@ -9,6 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec +import kotlin.run // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key @@ -35,8 +37,25 @@ open class Vidplay : ExtractorApi() { override val name = "Vidplay" override val mainUrl = "https://vidplay.site" override val requiresReferer = true - open val key = - "https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json" + + companion object { + private val keySource = "https://rowdy-avocado.github.io/multi-keys/" + + private var keys: List? = null + + private suspend fun getKeys(): List { + return keys + ?: run { + val res = + app.get(keySource).parsedSafe() + ?: throw ErrorLoadingException("Unable to get keys") + keys = res.keys + res.keys + } + } + + private data class KeysData(@JsonProperty("vidplay") val keys: List) + } override suspend fun getUrl( url: String, @@ -70,10 +89,6 @@ open class Vidplay : ExtractorApi() { } - private suspend fun getKeys(): List { - return app.get(key).parsed() - } - private suspend fun callFutoken(id: String, url: String): String? { val script = app.get("$mainUrl/futoken", referer = url).text val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null From 694193fa3eeada9388d68be521822ecf4f659bd4 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:40:41 +0530 Subject: [PATCH 117/157] refactor(fix): result sync, fix slider theme and trailer fix (#1187) --- app/build.gradle.kts | 24 +- app/src/main/res/layout/result_sync.xml | 397 ++++++++++-------------- 2 files changed, 168 insertions(+), 253 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ebefa0ea..6e439d53 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -157,16 +157,16 @@ dependencies { testImplementation("junit:junit:4.13.2") testImplementation("org.json:json:20240303") androidTestImplementation("androidx.test:core") - implementation("androidx.test.ext:junit-ktx:1.1.5") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("androidx.test.ext:junit-ktx:1.2.1") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") // Android Core & Lifecycle implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.2") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") // Design & UI @@ -182,9 +182,9 @@ dependencies { implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0") // For KSP -> Official Annotation Processors are Not Yet Supported for KSP - ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:33.2.0-android") - implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") + implementation("com.google.guava:guava:33.2.1-android") + implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") // Media 3 (ExoPlayer) implementation("androidx.media3:media3-ui:1.1.1") @@ -200,9 +200,9 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:592f159") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ - implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding + implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding // Crash Reports (AcraApplication.kt) implementation("ch.acra:acra-core:5.11.3") @@ -215,14 +215,14 @@ dependencies { implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview - implementation("io.github.g0dkar:qrcode-kotlin:4.1.1") // QR code for PIN Auth on TV + implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV // Extensions & Other Libs implementation("org.mozilla:rhino:1.7.15") // run JavaScript implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 - implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") // TMDB API v3 Wrapper Made with RetroFit + implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API diff --git a/app/src/main/res/layout/result_sync.xml b/app/src/main/res/layout/result_sync.xml index 9cde195c..8b7b33c0 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -1,306 +1,221 @@ + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/result_sync_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:visibility="gone" + tools:visibility="visible"> + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + android:id="@+id/result_sync_names" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:text="MyAnimeList, AniList" + android:textSize="16sp" + android:textStyle="bold" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:visibility="visible"> + android:id="@+id/result_sync_sub_episode" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|center_vertical" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:padding="10dp" + android:src="@drawable/baseline_remove_24" + app:tint="?attr/textColor" /> + android:id="@+id/result_sync_current_episodes" + style="@style/AppEditStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:inputType="number" + android:textColorHint="?attr/grayTextColor" + android:textSize="20sp" + tools:hint="20" + tools:ignore="LabelFor" /> + android:id="@+id/result_sync_max_episodes" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingBottom="1dp" + android:textColor="?attr/textColor" + android:textSize="20sp" + tools:text="30" /> + android:id="@+id/result_sync_add_episode" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|center_vertical" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:padding="10dp" + android:src="@drawable/ic_baseline_add_24" + app:tint="?attr/textColor" /> - - + android:id="@+id/result_sync_episodes" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="20dp" + android:layout_gravity="end|center_vertical" + android:indeterminate="false" + android:max="100" + android:padding="10dp" + android:progress="0" + android:progressBackgroundTint="?attr/colorPrimary" + tools:visibility="visible" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:padding="10dp" + android:text="@string/sync_score" + android:textColor="?attr/textColor" + android:textSize="17sp" /> + style="@style/BlackButton" + android:layout_width="wrap_content" + android:layout_height="30dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="0dp" + android:minWidth="0dp" + android:text="7/10" /> + android:id="@+id/result_sync_rating" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="-5dp" + android:layout_marginEnd="-5dp" + app:thumbHeight="20dp" + android:stepSize="1" + android:value="4" + android:valueFrom="0" + android:valueTo="10" + app:labelStyle="@style/BlackLabel" + app:thumbRadius="10dp" + app:tickVisible="false" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:visibility="gone"> + android:id="@+id/home_parent_item_title" + style="@style/WatchHeaderText" + tools:text="Recommended" /> + android:layout_width="30dp" + android:layout_height="match_parent" + android:layout_gravity="end|center_vertical" + android:layout_marginEnd="5dp" + android:contentDescription="@string/home_more_info" + android:src="@drawable/ic_baseline_arrow_forward_24" + app:tint="?attr/textColor" /> + android:id="@+id/result_sync_check" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + tools:listitem="@layout/sort_bottom_single_choice" /> + style="@style/WhiteButton" + android:layout_width="match_parent" + android:layout_marginTop="10dp" + android:text="@string/type_watching" + android:visibility="gone" /> + android:id="@+id/result_sync_set_score" + style="@style/BlackButton" + android:layout_width="match_parent" + android:layout_marginTop="10dp" + android:text="@string/upload_sync" + app:icon="@drawable/baseline_sync_24" /> + android:id="@+id/result_sync_loading_shimmer" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:orientation="vertical" + android:padding="15dp" + app:shimmer_auto_start="true" + app:shimmer_base_alpha="0.2" + app:shimmer_duration="@integer/loading_time" + app:shimmer_highlight_alpha="0.3" + tools:visibility="gone" + tools:ignore="MissingClass"> + + - - + android:layout_height="30dp" /> + android:layout_width="match_parent" + android:layout_height="30dp" /> + android:layout_width="match_parent" + android:layout_height="30dp" /> @@ -313,8 +228,8 @@ + android:layout_width="match_parent" + android:layout_height="30dp" /> From a157115cfac1ef3f3c532198a931873d6cee9097 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 15 Jul 2024 18:15:59 +0300 Subject: [PATCH 118/157] feat(Subtitles): SubSource subtitles provider (#1199) --- .../syncproviders/AccountManager.kt | 4 +- .../syncproviders/providers/SubSource.kt | 158 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index e86d73aa..0259ccad 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -22,6 +22,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() + val subSourceApi = SubSourceApi() // used to login via app intent val OAuth2Apis @@ -51,7 +52,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { get() = listOf( openSubtitlesApi, addic7ed, - subDlApi + subDlApi, + subSourceApi ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt new file mode 100644 index 00000000..0e233ece --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -0,0 +1,158 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.SubtitleHelper + +class SubSourceApi : AbstractSubProvider { + override val idPrefix = "subsource" + val name = "SubSource" + + companion object { + const val APIURL = "https://api.subsource.net/api" + const val DOWNLOADENDPOINT = "https://api.subsource.net/api/downloadSub" + } + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + + //Only supports Imdb Id search for now + if (query.imdbId == null) return null + val queryLang = SubtitleHelper.fromTwoLettersToLanguage(query.lang!!) + val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie + + val searchRes = app.post( + url = "$APIURL/searchMovie", + data = mapOf( + "query" to query.imdbId!! + ) + ).parsedSafe() ?: return null + + val postData = if (type == TvType.TvSeries) { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, + "season" to "season-${query.seasonNumber}" + ) + } else { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, + ) + } + + val getMovieRes = app.post( + url = "$APIURL/getMovie", + data = postData + ).parsedSafe().let { + // api doesn't has episode number or lang filtering + if (type == TvType.Movie) { + it?.subs?.filter { sub -> + sub.lang == queryLang + } + } else { + it?.subs?.filter { sub -> + sub.releaseName!!.contains( + String.format( + "E%02d", + query.epNumber + ) + ) && sub.lang == queryLang + } + } + } ?: return null + + return getMovieRes.map { subtitle -> + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = this.idPrefix, + name = subtitle.releaseName!!, + lang = subtitle.lang!!, + data = SubData( + movie = subtitle.linkName!!, + lang = subtitle.lang, + id = subtitle.subId.toString(), + ).toJson(), + type = type, + source = this.name, + epNumber = query.epNumber, + seasonNumber = query.seasonNumber, + isHearingImpaired = subtitle.hi == 1, + ) + } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + + val parsedSub = parseJson(data.data) + + val subRes = app.post( + url = "$APIURL/getSub", + data = mapOf( + "movie" to parsedSub.movie, + "lang" to data.lang, + "id" to parsedSub.id + ) + ).parsedSafe() ?: return + + this.addZipUrl( + "$DOWNLOADENDPOINT/${subRes.sub.downloadToken}" + ) { name, _ -> + name + } + } + + data class ApiSearch( + @JsonProperty("success") val success: Boolean, + @JsonProperty("found") val found: List, + ) + + data class Found( + @JsonProperty("id") val id: Long, + @JsonProperty("title") val title: String, + @JsonProperty("seasons") val seasons: Long, + @JsonProperty("type") val type: String, + @JsonProperty("releaseYear") val releaseYear: Long, + @JsonProperty("linkName") val linkName: String, + ) + + data class ApiResponse( + @JsonProperty("success") val success: Boolean, + @JsonProperty("movie") val movie: Movie, + @JsonProperty("subs") val subs: List, + ) + + data class Movie( + @JsonProperty("id") val id: Long? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("year") val year: Long? = null, + @JsonProperty("fullName") val fullName: String? = null, + ) + + data class Sub( + @JsonProperty("hi") val hi: Int? = null, + @JsonProperty("fullLink") val fullLink: String? = null, + @JsonProperty("linkName") val linkName: String? = null, + @JsonProperty("lang") val lang: String? = null, + @JsonProperty("releaseName") val releaseName: String? = null, + @JsonProperty("subId") val subId: Long? = null, + ) + + data class SubData( + @JsonProperty("movie") val movie: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("id") val id: String, + ) + + data class SubTitleLink( + @JsonProperty("sub") val sub: SubToken, + ) + + data class SubToken( + @JsonProperty("downloadToken") val downloadToken: String, + ) +} \ No newline at end of file From 627dd453093f3246afbc606d83fa13284ce16e5e Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Thu, 18 Jul 2024 02:02:35 +0200 Subject: [PATCH 119/157] 0bytes downloads fix --- .../utils/VideoDownloadManager.kt | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index f3cbdaf1..197bacc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -546,7 +546,8 @@ object VideoDownloadManager { tryResume: Boolean, ): StreamData { return setupStream( - context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"), + context.getBasePath().first ?: getDefaultDir(context) + ?: throw IOException("Bad config"), name, folder, extension, @@ -945,7 +946,7 @@ object VideoDownloadManager { bufferSize: Int = DEFAULT_BUFFER_SIZE, /** how many bytes bytes it should require to use the parallel downloader instead, * if we download a very small file we don't want it parallel */ - maximumSmallSize : Long = chuckSize * 2 + maximumSmallSize: Long = chuckSize * 2 ): LazyStreamDownloadData { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) @@ -1028,7 +1029,10 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, - parallelConnections: Int = 3 + parallelConnections: Int = 3, + /** how many bytes a valid file must be in bytes, + * this should be different for subtitles and video */ + minimumSize: Long = 100 ): DownloadStatus = withContext(Dispatchers.IO) { if (parallelConnections < 1) { return@withContext DOWNLOAD_INVALID_INPUT @@ -1074,6 +1078,13 @@ object VideoDownloadManager { ) ) + if (items.totalLength != null && items.totalLength < minimumSize) { + fileStream.closeQuietly() + metadata.onDelete() + stream.delete() + return@withContext DOWNLOAD_INVALID_INPUT + } + metadata.totalBytes = items.totalLength metadata.type = DownloadType.IsDownloading metadata.setDownloadFileInfoTemplate( @@ -1223,6 +1234,16 @@ object VideoDownloadManager { return@withContext DOWNLOAD_STOPPED } + // in case the head request lies about content-size, + // then we don't want shit output + if (metadata.bytesDownloaded < minimumSize) { + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + stream.delete() + return@withContext DOWNLOAD_INVALID_INPUT + } + metadata.type = DownloadType.IsDone return@withContext DOWNLOAD_SUCCESS } catch (e: IOException) { @@ -1274,6 +1295,7 @@ object VideoDownloadManager { val displayName = getDisplayName(name, extension) val stream = setupStream(baseFile, name, folder, extension, startAt > 0) + if (!stream.resume) startAt = 0 fileStream = stream.open() @@ -1300,6 +1322,7 @@ object VideoDownloadManager { ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() ) ) + val items = M3u8Helper2.hslLazy(listOf(m3u8)) metadata.hlsTotal = items.size @@ -1397,7 +1420,7 @@ object VideoDownloadManager { try { // may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling fileMutex.unlock() - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) } } @@ -1524,7 +1547,7 @@ object VideoDownloadManager { tryResume: Boolean = false, ): DownloadStatus { // no support for these file formats - if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { + if (link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { return DOWNLOAD_INVALID_INPUT } @@ -1556,7 +1579,7 @@ object VideoDownloadManager { } try { - when(link.type) { + when (link.type) { ExtractorLinkType.M3U8 -> { val startIndex = if (tryResume) { context.getKey( @@ -1576,6 +1599,7 @@ object VideoDownloadManager { callback, parallelConnections = maxConcurrentConnections ) } + ExtractorLinkType.VIDEO -> { return downloadThing( context, @@ -1585,9 +1609,13 @@ object VideoDownloadManager { "mp4", tryResume, ep.id, - callback, parallelConnections = maxConcurrentConnections + callback, + parallelConnections = maxConcurrentConnections, + /** We require at least 10 MB video files */ + minimumSize = (1 shl 20) * 10 ) } + else -> throw IllegalArgumentException("unsuported download type") } } catch (t: Throwable) { From 12de92455960f012bb0298723127061b90932327 Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Fri, 19 Jul 2024 09:10:34 -0700 Subject: [PATCH 120/157] updating vidplay encryption method (#1202) --- .../cloudstream3/extractors/VidSrcTo.kt | 2 +- .../cloudstream3/extractors/Vidplay.kt | 101 ++++++++---------- 2 files changed, 43 insertions(+), 60 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 578f5fb9..e974f23a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -40,7 +40,7 @@ class VidSrcTo : ExtractorApi() { val finalUrl = DecryptUrl(embedRes.result.encUrl) if(finalUrl.equals(embedRes.result.encUrl)) return@amap when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "F2Cloud" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) } } catch (e: Exception) { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt index 6202800f..d7e7ce18 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -4,13 +4,14 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import java.net.URLDecoder import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec -import kotlin.run +import kotlin.io.encoding.Base64 // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key @@ -33,6 +34,7 @@ class VidplayOnline : Vidplay() { override val mainUrl = "https://vidplay.online" } +@OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) open class Vidplay : ExtractorApi() { override val name = "Vidplay" override val mainUrl = "https://vidplay.site" @@ -58,83 +60,64 @@ open class Vidplay : ExtractorApi() { } override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit ) { + val myKeys = getKeys() + val domain = url.substringBefore("/e/") val id = url.substringBefore("?").substringAfterLast("/") - val encodeId = encodeId(id, getKeys()) - val mediaUrl = callFutoken(encodeId, url) - val res = app.get( - "$mediaUrl", headers = mapOf( - "Accept" to "application/json, text/javascript, */*; q=0.01", - "X-Requested-With" to "XMLHttpRequest", - ), referer = url - ).parsedSafe()?.result - + val encodedId = encode(id, myKeys.get(0)) + val t = url.substringAfter("t=").substringBefore("&") + val h = encode(id, myKeys.get(1)) + val mediaUrl = "$domain/mediainfo/$encodedId?t=$t&h=$h" + val encodedRes = + app.get("$mediaUrl").parsedSafe()?.result + ?: throw Exception("Unable to fetch link") + val decodedRes = decode(encodedRes, myKeys.get(2)) + val res = tryParseJson(decodedRes) res?.sources?.map { - M3u8Helper.generateM3u8( - this.name, - it.file ?: return@map, - "$mainUrl/" - ).forEach(callback) + M3u8Helper.generateM3u8(this.name, it.file ?: return@map, "$mainUrl/").forEach(callback) } res?.tracks?.filter { it.kind == "captions" }?.map { - subtitleCallback.invoke( - SubtitleFile(it.label ?: return@map, it.file ?: return@map) - ) + subtitleCallback.invoke(SubtitleFile(it.label ?: return@map, it.file ?: return@map)) } - } - private suspend fun callFutoken(id: String, url: String): String? { - val script = app.get("$mainUrl/futoken", referer = url).text - val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null - val a = mutableListOf(k) - for (i in id.indices) { - a.add((k[i % k.length].code + id[i].code).toString()) - } - return "$mainUrl/mediainfo/${a.joinToString(",")}?${url.substringAfter("?")}" + private fun encode(input: String, key: String): String { + val rc4Key = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.ENCRYPT_MODE, rc4Key) + val encryptedBytes = cipher.doFinal(input.toByteArray(Charsets.UTF_8)) + return Base64.UrlSafe.encode(encryptedBytes) } - private fun encodeId(id: String, keyList: List): String { - val cipher1 = Cipher.getInstance("RC4") - val cipher2 = Cipher.getInstance("RC4") - cipher1.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(keyList[0].toByteArray(), "RC4"), - cipher1.parameters - ) - cipher2.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(keyList[1].toByteArray(), "RC4"), - cipher2.parameters - ) - var input = id.toByteArray() - input = cipher1.doFinal(input) - input = cipher2.doFinal(input) - return base64Encode(input).replace("/", "_") + fun decode(input: String, key: String): String { + val decodedBytes = Base64.UrlSafe.decode(input) + val rc4Key = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key) + val decryptedBytes = cipher.doFinal(decodedBytes) + val decodedString = String(decryptedBytes, Charsets.UTF_8) + return URLDecoder.decode(decodedString, "UTF-8") } data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, ) data class Sources( - @JsonProperty("file") val file: String? = null, + @JsonProperty("file") val file: String? = null, ) data class Result( - @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), - @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), - ) - - data class Response( - @JsonProperty("result") val result: Result? = null, + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), + @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), ) + data class Response(@JsonProperty("result") val result: String? = null) } From 63465ed7a9cfb2121eb91015671872f9a14deefd Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:24:06 +0200 Subject: [PATCH 121/157] fix autohide --- .../lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 9 +++++++++ .../lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 1 + 2 files changed, 10 insertions(+) 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 75a861c0..97075136 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 @@ -728,6 +728,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var currentTapIndex = 0 protected fun autoHide() { currentTapIndex++ + delayHide() + } + + override fun playerStatusChanged() { + super.playerStatusChanged() + delayHide() + } + + private fun delayHide() { val index = currentTapIndex playerBinding?.playerHolder?.postDelayed({ if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) { 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 d827d31e..f6c78b07 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 @@ -158,6 +158,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerStatusChanged() { + super.playerStatusChanged() if (player.getIsPlaying()) { viewModel.forceClearCache = false } From 073af50f5f09a3ca6b4bc21d0c84ff1bf2264e8f Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:28:36 +0200 Subject: [PATCH 122/157] fixed html plot in preview --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 59f499c5..e8cbc4d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -107,6 +107,7 @@ import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setText +import com.lagradost.cloudstream3.ui.result.setTextHtml import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder @@ -1404,7 +1405,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa resultviewPreviewMetaDuration.setText(d.durationText) resultviewPreviewMetaRating.setText(d.ratingText) - resultviewPreviewDescription.setText(d.plotText) + resultviewPreviewDescription.setTextHtml(d.plotText) resultviewPreviewPoster.setImage( d.posterImage ?: d.posterBackgroundImage ) From bb8144a52ecd4f6518eaca41bebd7cdf2ce31e1d Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 19 Jul 2024 20:35:29 +0300 Subject: [PATCH 123/157] feat(TV UI): Player's Top controls redesign (#1203) --- .../cloudstream3/ui/player/CS3IPlayer.kt | 4 + .../ui/player/FullScreenPlayer.kt | 72 +++++++-- .../cloudstream3/ui/player/GeneratorPlayer.kt | 19 ++- .../cloudstream3/ui/player/IPlayer.kt | 2 + .../res/drawable/ic_baseline_equalizer_24.xml | 9 ++ .../res/drawable/ic_baseline_replay_24.xml | 9 ++ .../res/drawable/ic_baseline_restart_24.xml | 9 ++ .../drawable/ic_baseline_skip_next_24_big.xml | 10 ++ .../ic_baseline_skip_next_rounded_24.xml | 9 ++ .../main/res/layout/fragment_player_tv.xml | 11 +- .../main/res/layout/player_custom_layout.xml | 119 +++++++++++--- .../res/layout/player_custom_layout_tv.xml | 148 ++++++++++++++---- app/src/main/res/layout/subtitle_offset.xml | 31 ++-- .../main/res/layout/trailer_custom_layout.xml | 123 ++++++++++++--- app/src/main/res/values/strings.xml | 1 + 15 files changed, 467 insertions(+), 109 deletions(-) create mode 100644 app/src/main/res/drawable/ic_baseline_equalizer_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_replay_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_restart_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml create mode 100644 app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml 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 8e322f73..735e4095 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 @@ -912,7 +912,11 @@ class CS3IPlayer : IPlayer { } CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) + CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) + + CSPlayerEvent.Restart -> seekTo(0, source) + CSPlayerEvent.NextEpisode -> event( EpisodeSeekEvent( offset = 1, 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 97075136..a75b9899 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 @@ -14,7 +14,13 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.* +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.Surface +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -58,7 +64,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UserPreferenceDelegate import com.lagradost.cloudstream3.utils.Vector2 -import kotlin.math.* +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min +import kotlin.math.round const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage @@ -77,7 +87,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected open var isFullScreenPlayer = true protected var playerBinding: PlayerCustomLayoutBinding? = null - private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) + private var durationMode: Boolean by UserPreferenceDelegate("duration_mode", false) + // state of player UI protected var isShowing = false protected var isLocked = false @@ -243,7 +254,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() - playerBinding?.apply { playerOpenSource.let { ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply { @@ -284,7 +294,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.getCurrentPreferredSubtitle() == null } - private fun restoreOrientationWithSensor(activity: Activity){ + private fun restoreOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation var orientation = 0 when (currentOrientation) { @@ -300,7 +310,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { activity.requestedOrientation = orientation } - private fun toggleOrientationWithSensor(activity: Activity){ + private fun toggleOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation var orientation = 0 when (currentOrientation) { @@ -345,12 +355,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun updateOrientation(ignoreDynamicOrientation: Boolean = false) { activity?.apply { - if(lockRotation) { - if(isLocked) { + if (lockRotation) { + if (isLocked) { lockOrientation(this) - } - else { - if(ignoreDynamicOrientation){ + } else { + if (ignoreDynamicOrientation) { // restore when lock is disabled restoreOrientationWithSensor(this) } else { @@ -958,7 +967,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } else -> { - player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI) + player.handleEvent( + CSPlayerEvent.PlayPauseToggle, + PlayerEventSource.UI + ) } } } else if (doubleTapEnabled && isFullScreenPlayer) { @@ -1234,6 +1246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // if nothing has loaded these buttons should not be visible playerBinding?.apply { playerSkipEpisode.isVisible = false + playerGoForward.isVisible = false playerTracksBtt.isVisible = false playerSkipOp.isVisible = false shadowOverlay.isVisible = false @@ -1307,6 +1320,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.SeekBack) } + PlayerEventType.Restart -> { + player.handleEvent(CSPlayerEvent.Restart) + } + PlayerEventType.ToggleMute -> { player.handleEvent(CSPlayerEvent.ToggleMute) } @@ -1428,6 +1445,25 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } playerBinding?.apply { + + if (isLayout(TV or EMULATOR)) { + mapOf( + playerGoBack to playerGoBackText, + playerRestart to playerRestartText, + playerGoForward to playerGoForwardText + ).forEach { (button, text) -> + button.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + text.isSelected = false + text.isVisible = false + return@setOnFocusChangeListener + } + text.isSelected = true + text.isVisible = true + } + } + } + playerPausePlay.setOnClickListener { autoHide() player.handleEvent(CSPlayerEvent.PlayPauseToggle) @@ -1471,6 +1507,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.NextEpisode) } + playerGoForward.setOnClickListener { + autoHide() + player.handleEvent(CSPlayerEvent.NextEpisode) + } + + playerRestart.setOnClickListener { + autoHide() + player.handleEvent(CSPlayerEvent.Restart) + } + playerLock.setOnClickListener { autoHide() toggleLock() @@ -1564,7 +1610,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setRemainingTimeCounter(showRemaining: Boolean) { durationMode = showRemaining - playerBinding?.exoDuration?.isInvisible= showRemaining + playerBinding?.exoDuration?.isInvisible = showRemaining playerBinding?.timeLeft?.isVisible = showRemaining } 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 f6c78b07..1f7cc5bd 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 @@ -44,6 +44,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY @@ -1097,8 +1098,15 @@ class GeneratorPlayer : FullScreenPlayer() { } playerBinding?.playerSkipOp?.isVisible = isOpVisible - playerBinding?.playerSkipEpisode?.isVisible = - !isOpVisible && viewModel.hasNextEpisode() == true + + when { + isLayout(PHONE) -> + playerBinding?.playerSkipEpisode?.isVisible = + !isOpVisible && viewModel.hasNextEpisode() == true + + else -> + playerBinding?.playerGoForward?.isVisible = viewModel.hasNextEpisode() == true + } if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) { viewModel.preLoadNextLinks() @@ -1254,7 +1262,7 @@ class GeneratorPlayer : FullScreenPlayer() { fun setPlayerDimen(widthHeight: Pair?) { val extra = if (widthHeight != null) { val (width, height) = widthHeight - "${width}x${height}" + "- ${width}x${height}" } else { "" } @@ -1265,7 +1273,7 @@ class GeneratorPlayer : FullScreenPlayer() { 0 -> "" 1 -> extra 2 -> source - 3 -> "$source - $extra" + 3 -> "$source $extra" else -> "" } playerBinding?.playerVideoTitleRez?.apply { @@ -1290,7 +1298,8 @@ class GeneratorPlayer : FullScreenPlayer() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason - layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player + layout = + if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] 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 4bd5c769..5f7161f7 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 @@ -26,6 +26,7 @@ enum class PlayerEventType(val value: Int) { Resize(13), SearchSubtitlesOnline(14), SkipOp(15), + Restart(16), } enum class CSPlayerEvent(val value: Int) { @@ -39,6 +40,7 @@ enum class CSPlayerEvent(val value: Int) { PrevEpisode(6), PlayPauseToggle(7), ToggleMute(8), + Restart(9), } enum class CSPlayerLoading { diff --git a/app/src/main/res/drawable/ic_baseline_equalizer_24.xml b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml new file mode 100644 index 00000000..cd20ad15 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_replay_24.xml b/app/src/main/res/drawable/ic_baseline_replay_24.xml new file mode 100644 index 00000000..e247aa92 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_replay_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_restart_24.xml b/app/src/main/res/drawable/ic_baseline_restart_24.xml new file mode 100644 index 00000000..aed3a562 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_restart_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml new file mode 100644 index 00000000..a8c43bbd --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml new file mode 100644 index 00000000..452c4dd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_player_tv.xml b/app/src/main/res/layout/fragment_player_tv.xml index 07cbb3c3..3c0ac05e 100644 --- a/app/src/main/res/layout/fragment_player_tv.xml +++ b/app/src/main/res/layout/fragment_player_tv.xml @@ -68,7 +68,9 @@ android:layout_height="wrap_content" android:layout_margin="5dp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" + android:visibility="gone" + tools:visibility="visible"> diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 83be8832..be97b978 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -172,27 +172,108 @@ android:id="@+id/player_go_back_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="5dp" + android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + - @@ -626,7 +707,7 @@ android:nextFocusLeft="@id/player_sources_btt" android:nextFocusRight="@id/player_skip_op" android:text="@string/tracks" - app:icon="@drawable/ic_baseline_playlist_play_24" /> + app:icon="@drawable/ic_baseline_equalizer_24" /> + diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index d8406b35..98eb58ac 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -231,7 +231,7 @@ @@ -240,6 +240,7 @@ android:id="@+id/player_video_title" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAlignment="viewEnd" android:gravity="end" android:textColor="@color/white" android:textSize="16sp" @@ -250,6 +251,7 @@ android:id="@+id/player_video_title_rez" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAlignment="viewEnd" android:gravity="end" android:textColor="@color/white" android:textSize="16sp" @@ -285,28 +287,116 @@ android:id="@+id/player_go_back_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="5dp" + android:layout_marginStart="17dp" + android:layout_marginTop="20dp" + android:layout_marginEnd="17dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + - @@ -509,6 +599,7 @@ android:layout_height="wrap_content" android:layoutDirection="ltr" android:orientation="horizontal"> + + app:layout_constraintStart_toStartOf="parent" + app:tint="@color/player_button_tv" + tools:ignore="ContentDescription" /> + tools:text="-23:20" /> @@ -672,12 +763,13 @@ android:id="@+id/player_skip_episode" style="@style/VideoButtonTV" android:nextFocusLeft="@id/player_skip_op" - android:nextFocusRight="@id/player_resize_btt" android:nextFocusUp="@id/player_pause_play" android:nextFocusDown="@id/player_resize_btt" android:text="@string/next_episode" - app:icon="@drawable/ic_baseline_skip_next_24" /> + app:icon="@drawable/ic_baseline_skip_next_24" + android:visibility="gone" + tools:visibility="visible"/> + app:icon="@drawable/ic_baseline_equalizer_24" /> diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index d5e303b6..82c24e61 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -30,28 +30,27 @@ @@ -67,29 +66,29 @@ diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index 59104ca7..20b73630 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -137,33 +137,110 @@ - + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e3f788f..e68c22b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -96,6 +96,7 @@ Next Random @string/play_episode Go back + Play from the Beginning @string/home_change_provider_img_des Change Provider Preview Background From 4c7379c766837ffc11c62d4fa2c4e7aaa8afd5fc Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Sat, 20 Jul 2024 19:14:11 +0200 Subject: [PATCH 124/157] Revert #979 Episode download cache --- .../main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 1d23e503..802c1a64 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -67,8 +67,6 @@ object BackupUtils { OPEN_SUBTITLES_USER_KEY, SUBDL_SUBTITLES_USER_KEY, - DOWNLOAD_EPISODE_CACHE, - "biometric_key", // can lock down users if backup is shared on a incompatible device "nginx_user", // Nginx user key "download_path_key" // No access rights after restore data from backup @@ -266,4 +264,4 @@ object BackupUtils { } editor.apply() } -} \ No newline at end of file +} From 0c418fdf9bd41f6a94e9a8063c48bc4acd44ffb1 Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Sat, 20 Jul 2024 15:06:04 -0700 Subject: [PATCH 125/157] Updated VidSrc encryption methods (#1205) --- .../extractors/VidSrcExtractor.kt | 301 +++++++++++++----- 1 file changed, 227 insertions(+), 74 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt index a27bf188..5da919e2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt @@ -3,98 +3,251 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.* -import kotlinx.coroutines.delay -import java.net.URI +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.Base64 class VidSrcExtractor2 : VidSrcExtractor() { - override val mainUrl = "https://vidsrc.me/embed" - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val newUrl = url.lowercase().replace(mainUrl, super.mainUrl) - super.getUrl(newUrl, referer, subtitleCallback, callback) - } + override val mainUrl = "https://vidsrc.me" } open class VidSrcExtractor : ExtractorApi() { override val name = "VidSrc" - private val absoluteUrl = "https://v2.vidsrc.me" - override val mainUrl = "$absoluteUrl/embed" + override val mainUrl = "https://vidsrc.net" + private val apiUrl = "https://vidsrc.stream" override val requiresReferer = false - companion object { - /** Infinite function to validate the vidSrc pass */ - suspend fun validatePass(url: String) { - val uri = URI(url) - val host = uri.host - - // Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/ - val referer = host.split(".").let { - val size = it.size - "https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/" - } - - while (true) { - app.get(url, referer = referer) - delay(60_000) - } - } - } - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit ) { val iframedoc = app.get(url).document - val serverslist = - iframedoc.select("div#sources.button_content div#content div#list div").map { - val datahash = it.attr("data-hash") - if (datahash.isNotBlank()) { - val links = try { - app.get( - "$absoluteUrl/srcrcp/$datahash", - referer = "https://rcp.vidsrc.me/" - ).url - } catch (e: Exception) { - "" - } - links - } else "" - } + val srcrcpList = + iframedoc.select("div.serversList > div.server").mapNotNull { + val datahash = it.attr("data-hash") ?: return@mapNotNull null + val rcpLink = "$apiUrl/rcp/$datahash" + val rcpRes = app.get(rcpLink, referer = apiUrl).text + val srcrcpLink = + Regex("src:\\s*'(.*)',").find(rcpRes)?.destructured?.component1() + ?: return@mapNotNull null + "https:$srcrcpLink" + } - serverslist.amap { server -> - val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") - if (linkfixed.contains("/prorcp")) { - val srcresponse = app.get(server, referer = absoluteUrl).text - val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") - val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap - val passRegex = Regex("""['"](.*set_pass[^"']*)""") - val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace( - Regex("""^//"""), "https://" - ) + srcrcpList.amap { server -> + val res = app.get(server, referer = apiUrl) + if (res.url.contains("/prorcp")) { + val encodedElement = res.document.select("div#reporting_content+div") + val decodedUrl = + decodeUrl(encodedElement.attr("id"), encodedElement.text()) ?: return@amap callback.invoke( - ExtractorLink( - this.name, - this.name, - srcm3u8, - "https://vidsrc.stream/", - Qualities.Unknown.value, - extractorData = pass, - isM3u8 = true - ) + ExtractorLink( + this.name, + this.name, + decodedUrl, + apiUrl, + Qualities.Unknown.value, + isM3u8 = true + ) ) } else { - loadExtractor(linkfixed, url, subtitleCallback, callback) + loadExtractor(res.url, url, subtitleCallback, callback) } } } -} \ No newline at end of file + private fun decodeUrl(encType: String, url: String): String? { + return when (encType) { + "NdonQLf1Tzyx7bMG" -> bMGyx71TzQLfdonN(url) + "sXnL9MQIry" -> Iry9MQXnLs(url) + "IhWrImMIGL" -> IGLImMhWrI(url) + "xTyBxQyGTA" -> GTAxQyTyBx(url) + "ux8qjPHC66" -> C66jPHx8qu(url) + "eSfH1IRMyL" -> MyL1IRSfHe(url) + "KJHidj7det" -> detdj7JHiK(url) + "o2VSUnjnZl" -> nZlUnj2VSo(url) + "Oi3v1dAlaM" -> laM1dAi3vO(url) + "TsA2KGDGux" -> GuxKGDsA2T(url) + "JoAHUMCLXV" -> LXVUMCoAHJ(url) + else -> null + } + } + + private fun bMGyx71TzQLfdonN(a: String): String { + val b = 3 + val c = mutableListOf() + var d = 0 + while (d < a.length) { + c.add(a.substring(d, minOf(d + b, a.length))) + d += b + } + val e = c.reversed().joinToString("") + return e + } + + private fun Iry9MQXnLs(a: String): String { + val b = "pWB9V)[*4I`nJpp?ozyB~dbr9yt!_n4u" + val d = a.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + var c = "" + for (e in d.indices) { + c += (d[e].code xor b[e % b.length].code).toChar() + } + var e = "" + for (ch in c) { + e += (ch.code - 3).toChar() + } + return String(Base64.getDecoder().decode(e)) + } + + private fun IGLImMhWrI(a: String): String { + val b = a.reversed() + val c = + b + .map { + when (it) { + in 'a'..'m', in 'A'..'M' -> it + 13 + in 'n'..'z', in 'N'..'Z' -> it - 13 + else -> it + } + } + .joinToString("") + val d = c.reversed() + return String(Base64.getDecoder().decode(d)) + } + + private fun GTAxQyTyBx(a: String): String { + val b = a.reversed() + val c = b.filterIndexed { index, _ -> index % 2 == 0 } + return String(Base64.getDecoder().decode(c)) + } + + private fun C66jPHx8qu(a: String): String { + val b = a.reversed() + val c = "X9a(O;FMV2-7VO5x;Ao:dN1NoFs?j," + val d = b.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + var e = "" + for (i in d.indices) { + e += (d[i].code xor c[i % c.length].code).toChar() + } + return e + } + + private fun MyL1IRSfHe(a: String): String { + val b = a.reversed() + val c = b.map { (it.code - 1).toChar() }.joinToString("") + val d = c.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + return d + } + + private fun detdj7JHiK(a: String): String { + val b = a.substring(10, a.length - 16) + val c = "3SAY~#%Y(V%>5d/Yg\"\$G[Lh1rK4a;7ok" + val d = String(Base64.getDecoder().decode(b)) + val e = c.repeat((d.length + c.length - 1) / c.length).substring(0, d.length) + var f = "" + for (i in d.indices) { + f += (d[i].code xor e[i].code).toChar() + } + return f + } + + private fun nZlUnj2VSo(a: String): String { + val b = + mapOf( + 'x' to 'a', + 'y' to 'b', + 'z' to 'c', + 'a' to 'd', + 'b' to 'e', + 'c' to 'f', + 'd' to 'g', + 'e' to 'h', + 'f' to 'i', + 'g' to 'j', + 'h' to 'k', + 'i' to 'l', + 'j' to 'm', + 'k' to 'n', + 'l' to 'o', + 'm' to 'p', + 'n' to 'q', + 'o' to 'r', + 'p' to 's', + 'q' to 't', + 'r' to 'u', + 's' to 'v', + 't' to 'w', + 'u' to 'x', + 'v' to 'y', + 'w' to 'z', + 'X' to 'A', + 'Y' to 'B', + 'Z' to 'C', + 'A' to 'D', + 'B' to 'E', + 'C' to 'F', + 'D' to 'G', + 'E' to 'H', + 'F' to 'I', + 'G' to 'J', + 'H' to 'K', + 'I' to 'L', + 'J' to 'M', + 'K' to 'N', + 'L' to 'O', + 'M' to 'P', + 'N' to 'Q', + 'O' to 'R', + 'P' to 'S', + 'Q' to 'T', + 'R' to 'U', + 'S' to 'V', + 'T' to 'W', + 'U' to 'X', + 'V' to 'Y', + 'W' to 'Z' + ) + return a.map { b[it] ?: it }.joinToString("") + } + + private fun laM1dAi3vO(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 5 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } + + private fun GuxKGDsA2T(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 7 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } + + private fun LXVUMCoAHJ(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 3 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } +} From c8a863e332d0d952568385f438b2a035cca5b816 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 24 Jul 2024 22:38:16 +0200 Subject: [PATCH 126/157] Fixed ExampleInstrumentedTest --- .../java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index faacdf50..c7f02baf 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -154,7 +154,7 @@ class ExampleInstrumentedTest { fun providerCorrectHomepage() { runBlocking { getAllProviders().toList().amap { api -> - TestingUtils.testHomepage(api, ::println) + TestingUtils.testHomepage(api, TestingUtils.Logger()) } } println("Done providerCorrectHomepage") @@ -166,7 +166,6 @@ class ExampleInstrumentedTest { TestingUtils.getDeferredProviderTests( this, getAllProviders(), - ::println ) { _, _ -> } } } From dfd127265a066dfec18e797b3b2ddc7bf2ae51ef Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 25 Jul 2024 21:23:31 +0300 Subject: [PATCH 127/157] Trailers Fix (#1213) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6e439d53..1ad35d89 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -200,7 +200,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:592f159") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:2d36945") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding From e3ff1cf4554bc1bea1333a456c62becb987fd01b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 25 Jul 2024 21:23:49 +0300 Subject: [PATCH 128/157] feat(UI): Show Episode Runtime (#1207) --- .../metaproviders/TraktProvider.kt | 1 + .../cloudstream3/ui/result/EpisodeAdapter.kt | 15 +++++++-- .../cloudstream3/ui/result/ResultFragment.kt | 3 ++ .../ui/result/ResultViewModel2.kt | 6 ++-- .../main/res/layout/result_episode_large.xml | 30 +++++++++++++---- .../com/lagradost/cloudstream3/MainAPI.kt | 32 +++++++++++++++++-- 6 files changed, 75 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 7c375e0a..a1b9ff34 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -236,6 +236,7 @@ open class TraktProvider : MainAPI() { posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), rating = episode.rating?.times(10)?.roundToInt(), description = episode.overview, + runTime = episode.runtime ).apply { this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index ed5e51f1..06be6bd5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -27,7 +27,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper import java.text.DateFormat import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 const val ACTION_PLAY_EPISODE_IN_VLC_PLAYER = 2 @@ -58,6 +59,7 @@ const val ACTION_MARK_AS_WATCHED = 18 const val ACTION_FCAST = 19 const val TV_EP_SIZE = 400 + data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -274,7 +276,10 @@ class EpisodeAdapter( episodeDate.setText( txt( R.string.episode_upcoming_format, - secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + secondsToReadable( + card.airDate.minus(unixTimeMS).div(1000).toInt(), + "" + ) ) ) } else { @@ -292,6 +297,12 @@ class EpisodeAdapter( episodeDate.isVisible = false } + episodeRuntime.setText( + txt( + card.runTime?.times(60L)?.toInt()?.let { secondsToReadable(it, "") } + ) + ) + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) 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 c687eaa0..3eab0c71 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 @@ -51,6 +51,7 @@ data class ResultEpisode( /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, val airDate: Long? = null, + val runTime: Int? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -87,6 +88,7 @@ fun buildResultEpisode( parentId: Int, totalEpisodeIndex: Int? = null, airDate: Long? = null, + runTime: Int? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -111,6 +113,7 @@ fun buildResultEpisode( videoWatchState, totalEpisodeIndex, airDate, + runTime, ) } 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 8e8dfe30..5086426f 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 @@ -2371,7 +2371,8 @@ class ResultViewModel2 : ViewModel() { loadResponse.type, mainId, totalIndex, - airDate = i.date + airDate = i.date, + runTime = i.runTime, ) val season = eps.seasonIndex ?: 0 @@ -2426,7 +2427,8 @@ class ResultViewModel2 : ViewModel() { loadResponse.type, mainId, totalIndex, - airDate = episode.date + airDate = episode.date, + runTime = episode.runTime, ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index e5a6881a..935beac1 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -44,7 +44,7 @@ android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" tools:src="@drawable/example_poster" - tools:visibility="invisible"/> + tools:visibility="invisible" /> + tools:visibility="invisible" /> - + android:layout_gravity="start" + android:orientation="horizontal"> + + + + + + + Date: Thu, 25 Jul 2024 20:25:17 +0200 Subject: [PATCH 129/157] Add the option to hide video controls (#1210) --- .../ui/player/FullScreenPlayer.kt | 25 +++++++++++++++++++ .../ui/settings/SettingsPlayer.kt | 6 ++--- app/src/main/res/values-af/strings.xml | 1 + app/src/main/res/values-ajp/strings.xml | 1 + app/src/main/res/values-am/strings.xml | 1 + app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-ars/strings.xml | 1 + app/src/main/res/values-as/strings.xml | 1 + app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-bn/strings.xml | 1 + app/src/main/res/values-bp/strings.xml | 1 + app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-el/strings.xml | 1 + app/src/main/res/values-eo/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values-fil/strings.xml | 4 ++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-gl/strings.xml | 1 + app/src/main/res/values-hi/strings.xml | 1 + app/src/main/res/values-hr/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-in/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-iw/strings.xml | 1 + app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values-kn/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-lt/strings.xml | 1 + app/src/main/res/values-lv/strings.xml | 1 + app/src/main/res/values-mk/strings.xml | 1 + app/src/main/res/values-ml/strings.xml | 1 + app/src/main/res/values-ms/strings.xml | 1 + app/src/main/res/values-mt/strings.xml | 1 + app/src/main/res/values-my/strings.xml | 1 + app/src/main/res/values-ne/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-nn/strings.xml | 1 + app/src/main/res/values-no/strings.xml | 1 + app/src/main/res/values-or/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-qt/strings.xml | 1 + app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-so/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-ta/strings.xml | 1 + app/src/main/res/values-ti/strings.xml | 1 + app/src/main/res/values-tl/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-ur/strings.xml | 1 + app/src/main/res/values-vi/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/settings_player.xml | 6 +++++ 60 files changed, 93 insertions(+), 5 deletions(-) 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 a75b9899..ef7d6bc1 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 @@ -25,15 +25,18 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue import androidx.core.graphics.green import androidx.core.graphics.red +import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.preference.PreferenceManager +import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight @@ -120,6 +123,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var doubleTapPauseEnabled = true protected var playerRotateEnabled = false protected var autoPlayerRotateEnabled = false + private var hideControlsNames = false protected var subtitleDelay set(value) = try { @@ -1419,6 +1423,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { false ) + hideControlsNames = settingsManager.getBoolean(ctx.getString(R.string.hide_player_control_names_key), false) + val profiles = QualityDataHelper.getProfiles() val type = if (ctx.isUsingMobileData()) QualityDataHelper.QualityProfileType.Data @@ -1439,6 +1445,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() { playerSpeedBtt.isVisible = playBackSpeedEnabled playerResizeBtt.isVisible = playerResizeEnabled playerRotateBtt.isVisible = playerRotateEnabled + if (hideControlsNames) { + hideControlsNames() + } } } catch (e: Exception) { logError(e) @@ -1591,6 +1600,22 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + private fun PlayerCustomLayoutBinding.hideControlsNames() { + fun iterate(layout: LinearLayout) { + layout.children.forEach { + if (it is MaterialButton) { + it.textSize = 0f + it.iconPadding = 0 + it.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START + it.setPadding(0,0,0,0) + } else if (it is LinearLayout) { + iterate(it) + } + } + } + iterate(playerLockHolder.parent as LinearLayout) + } + override fun playerDimensionsLoaded(width: Int, height: Int) { isVerticalOrientation = height > width updateOrientation() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 20279cd1..7560d75f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -87,10 +87,6 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - /*(getPref(R.string.double_tap_seek_time_key) as? SeekBarPreference?)?.let { - - }*/ - getPref(R.string.prefer_limit_title_rez_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.limit_title_rez_pref_names) val prefValues = resources.getIntArray(R.array.limit_title_rez_pref_values) @@ -109,6 +105,8 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.hide_player_control_names_key)?.hideOn(TV) + getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 45e9a1d4..4adafee4 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -106,4 +106,5 @@ Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s Nuwe episode notifikasie + hide_player_control_names_key diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index c78b6924..718b5235 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -640,4 +640,5 @@ تجاهل متاح الريپوزيتوري فتاح %s ع تلفونك أو كمپيوترك، وحط الكود اللي فوق + hide_player_control_names_key diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 7fd3274b..26fb84dd 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -108,4 +108,5 @@ ተጨማሪ መረጃ ዓይነቶችን በመጠቀም ይፈልጉ ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ + hide_player_control_names_key diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c2ed35cb..e85fee04 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -666,4 +666,5 @@ قم بزيارة %s على هاتفك الذكي أو جهاز الكمبيوتر وأدخل الرمز أعلاه لا يمكن الحصول على رمز PIN للجهاز، حاول المصادقة المحلية تنتهي صلاحية الرمز خلال %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index f3811d3d..f028ef5d 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -352,4 +352,5 @@ وثائقي موقع عنوان مشغل الفيديو بحد أقصى لعدد الأحرف + hide_player_control_names_key diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 7fb0e7bd..dd1b2eed 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -621,4 +621,5 @@ ছাবটাইটেল বাছনি কৰক পৰ্ব খেলাওক প্ৰয়োগ কৰক + hide_player_control_names_key diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 66e29882..89801322 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -601,4 +601,5 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. + hide_player_control_names_key diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 3500e85a..1a02eebc 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -359,4 +359,5 @@ অ্যাকাউন্ট প্রস্থান %1$d%2$s + hide_player_control_names_key diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 6dc38cd8..51138312 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -656,4 +656,5 @@ Não é possível obter o código PIN do dispositivo, tente a autenticação local O código PIN expirou! O código expira em %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8e40b12b..2f7dcfed 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -658,4 +658,5 @@ Účty Lokální ověření PIN kód vypršel! + hide_player_control_names_key diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ee378ff6..12a68dbc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -615,4 +615,5 @@ Zurücksetzen Akkuverbrauch der App ist bereits auf unbeschränkt eingestellt CloudStreams App-Info kann nicht geöffnet werden. + hide_player_control_names_key diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e7fa1f6a..269626cb 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -625,4 +625,5 @@ Τα δεδομένα σας στο CloudStream έχουν κάνει back up. Αν και η πιθανότητα είναι πολύ χαμηλή, όλες οι συσκευές συμπεριφέρονται διαφορετικά. Στη σπάνια περίπτωση, που απαγορευτεί η πρόσβασή σας από την εφαρμογή, διαγράψτε τα δεδομένα εφαρμογής και επαναφέρετέ τα από ένα ήδη υπάρχον backup. Συγνώμη για οποιαδήποτε ταλαιπωρία. Λογαριασμοί Ασφάλεια + hide_player_control_names_key diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 275a4bfb..9d3d07bc 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -127,4 +127,5 @@ Elŝutite Elŝutante Elŝuto Malsukcesite + hide_player_control_names_key diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 82f29381..bd281b55 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -634,4 +634,5 @@ ¡El código PIN ya ha caducado! El código caduca en %1$d mín y %2$d s No puedo obtener el código PIN del dispositivo; intente con la autenticación local + hide_player_control_names_key diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index db432a61..86dee8ef 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -191,4 +191,5 @@ پیش‌فرض کارتون تورنت + hide_player_control_names_key diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 42eba3cc..2189dd75 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,2 +1,4 @@ - + + hide_player_control_names_key + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fa1e1b61..78f3f2a5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -620,4 +620,5 @@ Verrouillage biométrique Sélectionnez un appareil de diffusion Saison %1$d Episode %2$d sera publié dans + hide_player_control_names_key diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index ae3105cf..d04792f8 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -164,4 +164,5 @@ Selecciona o modo para filtrar a descarga dos complementos Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. Mostrar actualizacións da aplicación + hide_player_control_names_key diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e08a3b8b..bd50953c 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -209,4 +209,5 @@ रूपरेखा रंग उपशीर्षक ऊंचाई अक्षर शैली + hide_player_control_names_key diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 54448e58..90dbee79 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -651,4 +651,5 @@ CloudStream Wiki Računi Sigurnost + hide_player_control_names_key diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ebaff041..72213b02 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -592,4 +592,5 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása + hide_player_control_names_key diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 951ba417..0edae603 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -647,4 +647,5 @@ CloudStream Wiki Keamanan Akun + hide_player_control_names_key diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ff7ea6bd..8671a73a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -654,4 +654,5 @@ Impossibile ottenere il codice PIN del dispositivo, prova l\'autenticazione locale Il codice PIN è scaduto! Il codice scadrà tra %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 22626f50..1f34f0e1 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -550,4 +550,5 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! + hide_player_control_names_key diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index acb2cfc3..fb2ca02d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,5 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 + hide_player_control_names_key diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index f3fb665d..75f62bcc 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -130,4 +130,5 @@ Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ + hide_player_control_names_key diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bda82057..ec570e69 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -634,4 +634,5 @@ %s의 PIN 입력 즐겨찾기에서 제거 캐스트미러 + hide_player_control_names_key diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index f61bcfc0..0cb3addf 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -260,4 +260,5 @@ Ar tikrai norite išeiti\? Pašalinti iš žiūrimų Garso takelis + hide_player_control_names_key diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 566c721d..96272e71 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -527,4 +527,5 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s + hide_player_control_names_key diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 5e4d5c06..d4023ec4 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -624,4 +624,5 @@ Грешка при пристапот до таблата со исечоци, обидете се повторно. Грешка при копирање, копирајте го logcat и контактирајте со поддршката за апликацијата. Аудио книга + hide_player_control_names_key diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d97e666c..213d4a00 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -280,4 +280,5 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം + hide_player_control_names_key diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index dca98e53..aae74f4e 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -57,4 +57,5 @@ Tutup Ep cuba + hide_player_control_names_key diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml index b2c0356a..37da0580 100644 --- a/app/src/main/res/values-mt/strings.xml +++ b/app/src/main/res/values-mt/strings.xml @@ -123,4 +123,5 @@ Bookmarks Neħħi Falla t-tniżżil + hide_player_control_names_key diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 0ebe3c6b..e7007d12 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -550,4 +550,5 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် + hide_player_control_names_key diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 49cb6cfa..bc0199a1 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -128,4 +128,5 @@ प्लेयरको उपशीर्षकको सेटिङ रिपोजिटरी को नाम र यूआरएल कपी गरियो! + hide_player_control_names_key diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b685489b..6029f78b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -608,4 +608,5 @@ Link opnieuw geladen Autoroteer Roteer + hide_player_control_names_key diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 95c527f9..930841db 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -195,4 +195,5 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… + hide_player_control_names_key diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 7b013653..115cd2d3 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -538,4 +538,5 @@ Bruk Hjelp Profilbakgrunn + hide_player_control_names_key diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index bdc55780..07fc8a1d 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,5 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ + hide_player_control_names_key diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8e940c61..209c9d8e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -635,4 +635,5 @@ Odrzuć Otwórz repozytorium Odwiedź %s na swoim smartfonie lub komputerze i wprowadź powyższy kod + hide_player_control_names_key diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ce20a8af..59406383 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -621,4 +621,5 @@ Fcast Escolha o dispositivo Transmitir + hide_player_control_names_key diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 5de97c7d..258552e2 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,4 +247,5 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah + hide_player_control_names_key diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 344eae21..609190cf 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -641,4 +641,5 @@ Selectați divece-ul pe care doriți să faceți cast Cast mirror Fcast + hide_player_control_names_key diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5a9b843e..7f19ac8c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -622,4 +622,5 @@ Выйдет %s Fcast Выберите девайс для трансляции + hide_player_control_names_key diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a53e1f53..947e2b6d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -377,4 +377,5 @@ Pridať repozitár Názov repozitára Zobraziť komunitné repozitáre + hide_player_control_names_key diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index c750ea7a..5dc0bc23 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -485,4 +485,5 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka + hide_player_control_names_key diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 04230ab8..dd2dffb9 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -626,4 +626,5 @@ CloudStream Wiki Konton Säkerhet + hide_player_control_names_key diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 9378e400..44729e09 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -615,4 +615,5 @@ %கள் பிடித்தவைகளிலிருந்து அகற்றப்பட்டன உங்கள் கிளவுட்ச்ட்ரீம் தரவு இப்போது காப்புப் பிரதி எடுக்கப்பட்டுள்ளது. இதன் சாத்தியம் மிகக் குறைவு என்றாலும், எல்லா சாதனங்களும் வித்தியாசமாக நடந்து கொள்ளலாம். அரிய விசயத்தில், பயன்பாட்டை அணுகுவதிலிருந்து நீங்கள் பூட்டப்படுகிறீர்கள், பயன்பாட்டு தரவை முழுவதுமாக அழித்து, காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். இதிலிருந்து எழும் ஏதேனும் சிரமத்திற்கு நாங்கள் மிகவும் வருந்துகிறோம். ஊடகம் + hide_player_control_names_key diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml index 46235bbd..6c154c8d 100644 --- a/app/src/main/res/values-ti/strings.xml +++ b/app/src/main/res/values-ti/strings.xml @@ -3,4 +3,5 @@ %1$s ክፋል %2$d ክፋል %d በ ላይ ይወጣል ተዋሳእቲ፡ %s + hide_player_control_names_key diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index b4308eb7..dd964877 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -265,4 +265,5 @@ Mga Subtitle ng Chromecast Mga setting ng mga subtitle ng Chromecast Maglaro ng Trailer + hide_player_control_names_key diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3273a901..a55750e9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -681,4 +681,5 @@ Cihaz PIN kodu alınamıyor, yerel kimlik doğrulamayı deneyin PIN kodunun süresi doldu! Kodun süresi %1$dm %2$ds içinde doluyor + hide_player_control_names_key diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f5770e86..fd24274c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -634,4 +634,5 @@ Термін дії коду закінчується через %1$dхв %2$dс Автентифікація по місцю Відхилити + hide_player_control_names_key diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 04cfd381..c87be59c 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -617,4 +617,5 @@ دیگر ایکسٹینشنز میں تلاش کریں سفارشات دکھائیں آپ کے CloudStream ڈیٹا کا اب بیک اپ لیا گیا ہے۔ اگرچہ اس کا امکان بہت کم ہے، لیکن مختلف ڈیوائس مختلف طریقے سے کام کر سکتے ہیں۔ اگر آپ ایپ تک رسائی حاصل کرنے سے قاصر ہیں تو، ایپ کا ڈیٹا مکمل طور پر صاف کریں اور بیک اپ سے بحال کریں۔ اس سے ہونے والی کسی بھی تکلیف کے لیے ہم بہت معذرت خواہ ہیں۔ + hide_player_control_names_key diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 92e088bf..44868647 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -643,4 +643,5 @@ Truy cập %s trên điện thoại hoặc máy tính và nhập mã bên trên Mã PIN đã hết hạn! Mã sẽ hết hạn trong %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c50f284c..69eb8741 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -671,4 +671,5 @@ 為了確保下載與通知已訂閱的電視節目的不間斷,CloudStream 需要取得在背景執行的權限。若點選「確定」,將移至「應用程式資訊」,請找到「應用程式電池使用」並將電池用量設置為「無限制」。請注意,取得此權限並不表示 CS3 會明顯增加電池用量,而是只在必要時在背景執行,例如取得通知或使用官方擴充功能下載影片時。若選擇「取消」,您可以稍後在「一般設定」中調整此設定。 CloudStream Wiki 此裝置不支援生物特徵認證 + hide_player_control_names_key diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 97ba24ea..f2db04e2 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -673,4 +673,5 @@ 选择投射设备 %1$d季%2$d集将在 投射镜像 + hide_player_control_names_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e68c22b9..21067fff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,4 +797,6 @@ Can\'t get the device PIN code, try local authentication PIN code is now expired ! Code expires in %1$dm %2$ds + hide_player_control_names_key + Hide names of the player\'s controls \ No newline at end of file diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 5d5b11d0..0039af3a 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -37,6 +37,12 @@ android:icon="@drawable/ic_baseline_text_format_24" android:key="@string/prefer_limit_title_rez_key" android:title="@string/limit_title_rez" /> + Date: Thu, 25 Jul 2024 20:26:21 +0200 Subject: [PATCH 130/157] Bump 4.4.0 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1ad35d89..2040cf39 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,8 +60,8 @@ android { minSdk = 21 targetSdk = 33 /* Android 14 is Fu*ked ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/ - versionCode = 63 - versionName = "4.3.2" + versionCode = 64 + versionName = "4.4.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From a28ee413680da64d059bdc90510f67b816e62568 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:59:37 -0600 Subject: [PATCH 131/157] Fix for navigation UI bug (#1220) --- .../lagradost/cloudstream3/MainActivity.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index e8cbc4d8..bc2cb88e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -572,6 +572,35 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa binding?.apply { navRailView.isVisible = isNavVisible && landscape navView.isVisible = isNavVisible && !landscape + + /** + * We need to make sure if we return to a sub-fragment, + * the correct navigation item is selected so that it does not + * highlight the wrong one in UI. + */ + when (destination.id) { + in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> { + navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true + navView.menu.findItem(R.id.navigation_downloads).isChecked = true + } + in listOf( + R.id.navigation_settings, + R.id.navigation_subtitles, + R.id.navigation_chrome_subtitles, + R.id.navigation_settings_player, + R.id.navigation_settings_updates, + R.id.navigation_settings_ui, + R.id.navigation_settings_account, + R.id.navigation_settings_providers, + R.id.navigation_settings_general, + R.id.navigation_settings_extensions, + R.id.navigation_settings_plugins, + R.id.navigation_test_providers + ) -> { + navRailView.menu.findItem(R.id.navigation_settings).isChecked = true + navView.menu.findItem(R.id.navigation_settings).isChecked = true + } + } } } From 0aa48f335a818e0ebf0e1cf045d302a782e79857 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:26:22 -0600 Subject: [PATCH 132/157] Fix subscription icon displaying for movie types in result previews (#1222) --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5086426f..ce0fbdc5 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 @@ -2163,7 +2163,7 @@ class ResultViewModel2 : ViewModel() { // lets say that we have subscribed, then we must be able to unsubscribe no matter what else if (data != null) { _subscribeStatus.postValue(true) - } + } else _subscribeStatus.postValue(null) } private fun postFavorites(loadResponse: LoadResponse) { @@ -2861,4 +2861,4 @@ class ResultViewModel2 : ViewModel() { } } } -} +} \ No newline at end of file From 04dda008c40bae83ca076c24f2c8f75f6fcdb870 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:39:04 +0200 Subject: [PATCH 133/157] Clean up and mark questionable code issues (#1209) --- .../lagradost/cloudstream3/AcraApplication.kt | 12 +- .../lagradost/cloudstream3/CommonActivity.kt | 16 +- .../cloudstream3/DownloaderTestImpl.kt | 6 +- .../lagradost/cloudstream3/MainActivity.kt | 49 +++---- .../cloudstream3/NativeCrashHandler.kt | 53 ------- .../metaproviders/SyncRedirector.kt | 6 +- .../metaproviders/TraktProvider.kt | 2 +- .../cloudstream3/network/CloudflareKiller.kt | 6 +- .../cloudstream3/plugins/CloudstreamPlugin.kt | 3 +- .../lagradost/cloudstream3/plugins/Plugin.kt | 10 +- .../cloudstream3/plugins/PluginManager.kt | 34 +++-- .../cloudstream3/plugins/RepositoryManager.kt | 2 +- .../cloudstream3/plugins/VotingApi.kt | 9 +- .../subtitles/AbstractSubProvider.kt | 2 +- .../subtitles/AbstractSubtitleEntities.kt | 1 - .../syncproviders/AccountManager.kt | 12 +- .../syncproviders/providers/Addic7ed.kt | 14 +- .../syncproviders/providers/AniListApi.kt | 40 ++--- .../syncproviders/providers/LocalList.kt | 2 - .../syncproviders/providers/MALApi.kt | 128 ++++++++-------- .../providers/OpenSubtitlesApi.kt | 29 ++-- .../syncproviders/providers/SimklApi.kt | 137 +++++++++--------- .../syncproviders/providers/SubSource.kt | 1 + .../cloudstream3/ui/APIRepository.kt | 6 +- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 2 + .../cloudstream3/ui/ControllerActivity.kt | 2 + .../cloudstream3/ui/CustomRecyclerViews.kt | 4 +- .../cloudstream3/ui/EasterEggMonke.kt | 2 +- .../ui/NonFinalAdapterListUpdateCallback.kt | 2 +- .../lagradost/cloudstream3/ui/WatchType.kt | 4 +- .../cloudstream3/ui/WebviewFragment.kt | 3 + .../ui/download/button/BaseFetchButton.kt | 1 + .../ui/download/button/DownloadButton.kt | 2 +- .../ui/home/HomeChildItemAdapter.kt | 5 +- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../ui/home/HomeParentItemAdapter.kt | 13 +- .../ui/home/HomeParentItemAdapterPreview.kt | 7 +- .../cloudstream3/ui/home/HomeViewModel.kt | 10 +- .../ui/library/LibraryFragment.kt | 6 +- .../ui/library/ViewpagerAdapter.kt | 3 +- .../ui/player/AbstractPlayerFragment.kt | 7 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 82 ++--------- .../ui/player/CustomSubtitleDecoderFactory.kt | 7 +- .../ui/player/CustomTextRenderer.kt | 3 +- .../ui/player/DownloadFileGenerator.kt | 2 +- .../ui/player/DownloadedPlayerActivity.kt | 4 - .../ui/player/FullScreenPlayer.kt | 42 +++--- .../cloudstream3/ui/player/GeneratorPlayer.kt | 20 ++- .../cloudstream3/ui/player/IPlayer.kt | 1 - .../cloudstream3/ui/player/LinkGenerator.kt | 1 - .../ui/player/NonFinalTextRenderer.java | 16 +- .../ui/player/OfflinePlaybackHelper.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 2 +- .../ui/player/PlayerSubtitleHelper.kt | 3 + .../ui/player/PreviewGenerator.kt | 21 ++- .../ui/player/RepoLinkGenerator.kt | 1 + .../player/source_priority/PriorityAdapter.kt | 5 - .../player/source_priority/ProfilesAdapter.kt | 2 - .../source_priority/QualityDataHelper.kt | 7 +- .../source_priority/QualityProfileDialog.kt | 2 +- .../source_priority/SourcePriorityDialog.kt | 2 +- .../cloudstream3/ui/result/ActorAdaptor.kt | 5 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 6 +- .../cloudstream3/ui/result/ImageAdapter.kt | 15 +- .../ui/result/ResultFragmentPhone.kt | 49 ++++--- .../ui/result/ResultFragmentTv.kt | 45 +----- .../ui/result/ResultViewModel2.kt | 2 +- .../cloudstream3/ui/result/SelectAdaptor.kt | 3 +- .../cloudstream3/ui/search/SearchAdaptor.kt | 7 +- .../ui/search/SearchHistoryAdaptor.kt | 8 +- .../ui/search/SearchResultBuilder.kt | 7 +- .../ui/search/SyncSearchViewModel.kt | 4 +- .../ui/settings/AccountAdapter.kt | 7 +- .../ui/settings/SettingsFragment.kt | 1 - .../ui/settings/SettingsGeneral.kt | 2 - .../ui/settings/SettingsPlayer.kt | 9 +- .../ui/settings/SettingsProviders.kt | 2 +- .../ui/settings/SettingsUpdates.kt | 10 +- .../ui/settings/extensions/PluginAdapter.kt | 23 +-- .../ui/settings/extensions/PluginsFragment.kt | 2 +- .../settings/extensions/PluginsViewModel.kt | 1 - .../ui/setup/SetupFragmentMedia.kt | 1 - .../subtitles/ChromecastSubtitlesFragment.kt | 29 ++-- .../ui/subtitles/SubtitlesFragment.kt | 9 +- .../lagradost/cloudstream3/utils/AniSkip.kt | 2 +- .../cloudstream3/utils/AppContextUtils.kt | 5 +- .../cloudstream3/utils/BackupUtils.kt | 36 ++--- .../lagradost/cloudstream3/utils/DataStore.kt | 25 +++- .../utils/DownloadFileWorkManager.kt | 1 - .../cloudstream3/utils/InAppUpdater.kt | 40 ++--- .../cloudstream3/utils/PackageInstaller.kt | 5 +- .../cloudstream3/utils/PowerManagerAPI.kt | 6 +- .../lagradost/cloudstream3/utils/SyncUtil.kt | 8 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 2 +- .../utils/VideoDownloadManager.kt | 8 +- .../cloudstream3/widget/FlowLayout.kt | 2 +- .../cloudstream3/PluginAdapterTest.kt | 16 ++ 97 files changed, 563 insertions(+), 721 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt create mode 100644 app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 598ff540..d6f978fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -35,6 +35,7 @@ import java.io.File import java.io.FileNotFoundException import java.io.PrintStream import java.lang.ref.WeakReference +import java.util.Locale import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -81,14 +82,8 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : ACRA.errorReporter.handleException(error) try { PrintStream(errorFile).use { ps -> - ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) - ps.println( - String.format( - "Fatal exception on thread %s (%d)", - thread.name, - thread.id - ) - ) + ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}") + ps.println("Fatal exception on thread ${thread.name} (${thread.id})") error.printStackTrace(ps) } } catch (ignored: FileNotFoundException) { @@ -106,7 +101,6 @@ class AcraApplication : Application() { override fun onCreate() { super.onCreate() - //NativeCrashHandler.initCrashHandler() ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index ba303fef..63912114 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -164,7 +164,7 @@ object CommonActivity { val toast = Toast(act) toast.duration = duration ?: Toast.LENGTH_SHORT toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) - toast.view = binding.root + toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version. currentToast = toast toast.show() @@ -464,20 +464,6 @@ object CommonActivity { fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { - //println("Keycode: $keyCode") - //showToast( - // this, - // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}", - // Toast.LENGTH_LONG - //) - - // Tested keycodes on remote: - // KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - // KeyEvent.KEYCODE_MEDIA_REWIND - // KeyEvent.KEYCODE_MENU - // KeyEvent.KEYCODE_MEDIA_NEXT - // KeyEvent.KEYCODE_MEDIA_PREVIOUS - // KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE // 149 keycode_numpad 5 when (keyCode) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 934dd58a..8da7ca38 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() { - private val client: OkHttpClient + private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build() override fun execute(request: Request): Response { val httpMethod: String = request.httpMethod() val url: String = request.url() @@ -74,8 +74,4 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do return instance } } - - init { - client = builder.readTimeout(30, TimeUnit.SECONDS).build() - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index bc2cb88e..eed69a50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -82,13 +82,13 @@ import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.services.SubscriptionWorkManager +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi import com.lagradost.cloudstream3.syncproviders.SyncAPI @@ -347,7 +347,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa println("Repository url: $realUrl") loadRepository(realUrl) return true - } else if (str.contains(appString)) { + } else if (str.contains(APP_STRING)) { for (api in OAuth2Apis) { if (str.contains("/${api.redirectUrl}")) { ioSafe { @@ -377,15 +377,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } // This specific intent is used for the gradle deployWithAdb // https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46 - if (str == "$appString:") { + if (str == "$APP_STRING:") { PluginManager.hotReloadAllLocalPlugins(activity) } - } else if (safeURI(str)?.scheme == appStringRepo) { - val url = str.replaceFirst(appStringRepo, "https") + } else if (safeURI(str)?.scheme == APP_STRING_REPO) { + val url = str.replaceFirst(APP_STRING_REPO, "https") loadRepository(url) return true - } else if (safeURI(str)?.scheme == appStringSearch) { - val query = str.substringAfter("$appStringSearch://") + } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) { + val query = str.substringAfter("$APP_STRING_SEARCH://") nextSearchQuery = try { URLDecoder.decode(query, "UTF-8") @@ -399,7 +399,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa R.id.navigation_search activity?.findViewById(R.id.nav_rail_view)?.selectedItemId = R.id.navigation_search - } else if (safeURI(str)?.scheme == appStringPlayer) { + } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) { val uri = Uri.parse(str) val name = uri.getQueryParameter("name") val url = URLDecoder.decode(uri.authority, "UTF-8") @@ -413,9 +413,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) ) ) - } else if (safeURI(str)?.scheme == appStringResumeWatching) { + } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) { val id = - str.substringAfter("$appStringResumeWatching://").toIntOrNull() + str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull() ?: return false ioSafe { val resumeWatchingCard = @@ -469,7 +469,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) DubStatus.Dubbed else DubStatus.Subbed, null ) } else { - viewModel.loadSmall(this, result) + viewModel.loadSmall(result) } } @@ -605,7 +605,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } //private var mCastSession: CastSession? = null - lateinit var mSessionManager: SessionManager + var mSessionManager: SessionManager? = null private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() } private inner class SessionManagerListenerImpl : SessionManagerListener { @@ -645,8 +645,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa setActivityInstance(this) try { if (isCastApiAvailable()) { - //mCastSession = mSessionManager.currentCastSession - mSessionManager.addSessionManagerListener(mSessionManagerListener) + mSessionManager?.addSessionManagerListener(mSessionManagerListener) } } catch (e: Exception) { logError(e) @@ -662,7 +661,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } try { if (isCastApiAvailable()) { - mSessionManager.removeSessionManagerListener(mSessionManagerListener) + mSessionManager?.removeSessionManagerListener(mSessionManagerListener) //mCastSession = null } } catch (e: Exception) { @@ -766,7 +765,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa list.forEach { custom -> allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } ?.let { - allProviders.add(it.javaClass.newInstance().apply { + allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply { name = custom.name lang = custom.lang mainUrl = custom.url.trimEnd('/') @@ -1147,7 +1146,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa super.onCreate(savedInstanceState) try { if (isCastApiAvailable()) { - mSessionManager = CastContext.getSharedInstance(this).sessionManager + CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager } } } catch (t: Throwable) { logError(t) @@ -1449,13 +1448,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa val value = viewModel.watchStatus.value ?: WatchType.NONE this@MainActivity.showBottomDialog( - WatchType.values().map { getString(it.stringRes) }.toList(), + WatchType.entries.map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { viewModel.updateWatchStatus( - WatchType.values()[it], + WatchType.entries[it], this@MainActivity ) } @@ -1465,12 +1464,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ?: SyncWatchType.NONE this@MainActivity.showBottomDialog( - SyncWatchType.values().map { getString(it.stringRes) }.toList(), + SyncWatchType.entries.map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - syncViewModel.setStatus(SyncWatchType.values()[it].internalId) + syncViewModel.setStatus(SyncWatchType.entries[it].internalId) syncViewModel.publishUserData() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt deleted file mode 100644 index 7be90440..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.lagradost.cloudstream3 - -import com.lagradost.cloudstream3.MainActivity.Companion.lastError -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -object NativeCrashHandler { - // external fun triggerNativeCrash() - /*private external fun initNativeCrashHandler() - private external fun getSignalStatus(): Int - - private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { - - //launch { - // delay(10000) - // triggerNativeCrash() - //} - - while (true) { - delay(10_000) - val signal = getSignalStatus() - // Signal is initialized to zero - if (signal == 0) continue - - // Do not crash in safe mode! - if (lastError != null) continue - if (checkSafeModeFile()) continue - - AcraApplication.exceptionHandler?.uncaughtException( - Thread.currentThread(), - RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") - ) - } - } - - fun initCrashHandler() { - try { - System.loadLibrary("native-lib") - initNativeCrashHandler() - } catch (t: Throwable) { - // Make debug crash. - if (BuildConfig.DEBUG) throw t - logError(t) - return - } - - initSignalPolling() - }*/ -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt index 75e96bec..bc646a8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt @@ -2,15 +2,13 @@ package com.lagradost.cloudstream3.metaproviders import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncIdName object SyncRedirector { - val syncApis = SyncApis private val syncIds = listOf( - SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""), - SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""") + SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""), + SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""") ) suspend fun redirect( diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index a1b9ff34..addee9a0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -296,7 +296,7 @@ open class TraktProvider : MainAPI() { return try { val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) val dateTime = dateString?.let { format.parse(it)?.time } ?: return false - APIHolder.unixTimeMS < dateTime + unixTimeMS < dateTime } catch (t: Throwable) { logError(t) false diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index ce2fb3a2..85a9db5d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.network -import android.util.Base64 import android.util.Log import android.webkit.CookieManager import androidx.annotation.AnyThread @@ -10,7 +9,10 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.cookies import kotlinx.coroutines.runBlocking -import okhttp3.* +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response import java.net.URI diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt index e89ccfeb..ddf5b286 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt @@ -2,5 +2,4 @@ package com.lagradost.cloudstream3.plugins @Suppress("unused") @Target(AnnotationTarget.CLASS) -annotation class CloudstreamPlugin( -) \ No newline at end of file +annotation class CloudstreamPlugin \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 7f08af92..fc836587 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -34,7 +34,7 @@ abstract class Plugin { */ fun registerMainAPI(element: MainAPI) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") - element.sourcePlugin = this.__filename + element.sourcePlugin = this.filename // Race condition causing which would case duplicates if not for distinctBy synchronized(APIHolder.allProviders) { APIHolder.allProviders.add(element) @@ -48,7 +48,7 @@ abstract class Plugin { */ fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") - element.sourcePlugin = this.__filename + element.sourcePlugin = this.filename extractorApis.add(element) } @@ -68,7 +68,11 @@ abstract class Plugin { */ var resources: Resources? = null /** Full file path to the plugin. */ - var __filename: String? = null + @Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename")) + var __filename: String? + get() = filename + set(value) {filename = value} + var filename: String? = null /** * This will add a button in the settings allowing you to add custom settings diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 6b2b75f2..bc2a1780 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -1,13 +1,16 @@ package com.lagradost.cloudstream3.plugins +import android.Manifest import android.app.* import android.content.Context +import android.content.pm.PackageManager import android.content.res.AssetManager import android.content.res.Resources import android.os.Build import android.os.Environment import android.util.Log import android.widget.Toast +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.FragmentActivity @@ -163,7 +166,7 @@ object PluginManager { private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins" - public var currentlyLoading: String? = null + var currentlyLoading: String? = null // Maps filepath to plugin val plugins: MutableMap = @@ -339,7 +342,7 @@ object PluginManager { //Omit non-NSFW if mode is set to NSFW only if (mode == AutoDownloadMode.NsfwOnly) { - if (tvtypes.contains(TvType.NSFW.name) == false) { + if (!tvtypes.contains(TvType.NSFW.name)) { return@mapNotNull null } } @@ -504,10 +507,12 @@ object PluginManager { val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also { Log.d(TAG, "No manifest version for ${data.internalName}") } + + @Suppress("UNCHECKED_CAST") val pluginClass: Class<*> = loader.loadClass(manifest.pluginClassName) as Class val pluginInstance: Plugin = - pluginClass.newInstance() as Plugin + pluginClass.getDeclaredConstructor().newInstance() as Plugin // Sets with the proper version setPluginData(data.copy(version = version)) @@ -517,14 +522,16 @@ object PluginManager { return true } - pluginInstance.__filename = file.absolutePath + pluginInstance.filename = file.absolutePath if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk - val assets = AssetManager::class.java.newInstance() + val assets = AssetManager::class.java.getDeclaredConstructor().newInstance() val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java) addAssetPath.invoke(assets, file.absolutePath) + + @Suppress("DEPRECATION") pluginInstance.resources = Resources( assets, context.resources.displayMetrics, @@ -566,14 +573,14 @@ object PluginManager { // remove all registered apis synchronized(APIHolder.apis) { - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { removePluginMapping(it) } } synchronized(APIHolder.allProviders) { - APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename } + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename } + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } classLoaders.values.removeIf { v -> v == plugin } @@ -720,9 +727,14 @@ object PluginManager { } val notification = builder.build() - with(NotificationManagerCompat.from(context)) { - // notificationId is a unique int for each notification that you must define - notify((System.currentTimeMillis() / 1000).toInt(), notification) + // notificationId is a unique int for each notification that you must define + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(context) + .notify((System.currentTimeMillis() / 1000).toInt(), notification) } return notification } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index b80a590e..c6ec9df7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -73,7 +73,7 @@ object RepositoryManager { val PREBUILT_REPOSITORIES: Array by lazy { getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } - val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") + private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */ fun convertRawGitUrl(url: String): String { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index a45ab5f0..d1b702f4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock object VotingApi { // please do not cheat the votes lol private const val LOGKEY = "VotingApi" - private const val apiDomain = "https://counterapi.com/api" + private const val API_DOMAIN = "https://counterapi.com/api" private fun transformUrl(url: String): String = // dont touch or all votes get reset MessageDigest @@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol .joinToString("-") private suspend fun readVote(pluginUrl: String): Int { - var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" + val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value ?: 0 } private suspend fun writeVote(pluginUrl: String): Boolean { - var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" + val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value != null } @@ -69,8 +69,7 @@ object VotingApi { // please do not cheat the votes lol getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false fun canVote(pluginUrl: String): Boolean { - if (!PluginManager.urlPlugins.contains(pluginUrl)) return false - return true + return PluginManager.urlPlugins.contains(pluginUrl) } private val voteLock = Mutex() diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt index 857fba11..df64caab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt @@ -59,7 +59,7 @@ class SubtitleResource { return file } - fun unzip(file: File): List> { + private fun unzip(file: File): List> { val entries = mutableListOf>() ZipInputStream(file.inputStream()).use { zipInputStream -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt index ed4ccb74..685b499b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.subtitles -import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.TvType class AbstractSubtitleEntities { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 0259ccad..2e14c3c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -56,22 +56,22 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { subSourceApi ) - const val appString = "cloudstreamapp" - const val appStringRepo = "cloudstreamrepo" - const val appStringPlayer = "cloudstreamplayer" + const val APP_STRING = "cloudstreamapp" + const val APP_STRING_REPO = "cloudstreamrepo" + const val APP_STRING_PLAYER = "cloudstreamplayer" // Instantly start the search given a query - const val appStringSearch = "cloudstreamsearch" + const val APP_STRING_SEARCH = "cloudstreamsearch" // Instantly resume watching a show - const val appStringResumeWatching = "cloudstreamcontinuewatching" + const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching" val unixTime: Long get() = System.currentTimeMillis() / 1000L val unixTimeMs: Long get() = System.currentTimeMillis() - const val maxStale = 60 * 10 + const val MAX_STALE = 60 * 10 fun secondsToReadable(seconds: Int, completedValue: String): String { var secondsLong = seconds.toLong() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt index 507c5e2a..db467639 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt @@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi { override fun logOut() {} companion object { - const val host = "https://www.addic7ed.com" + const val HOST = "https://www.addic7ed.com" const val TAG = "ADDIC7ED" } private fun fixUrl(url: String): String { - return if (url.startsWith("/")) host + url - else if (!url.startsWith("http")) "$host/$url" + return if (url.startsWith("/")) HOST + url + else if (!url.startsWith("http")) "$HOST/$url" else url } @@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi { } val title = queryText.substringBefore("(").trim() - val url = "$host/search.php?search=${title}&Submit=Search" + val url = "$HOST/search.php?search=${title}&Submit=Search" val hostDocument = app.get(url).document var searchResult = "" if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url @@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi { hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(") ?.substringBefore(",") val doc = app.get( - "$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", - referer = "$host/" + "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", + referer = "$HOST/" ).document doc.select("#season tr:contains($queryLang)").mapNotNull { node -> if (node.selectFirst("td")?.text() @@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi { val link = fixUrl(node.select("a.buttonDownload").attr("href")) val isHearingImpaired = !node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty() - cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired) + cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired) } return results } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 8a82cf94..e51d3d65 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -63,7 +63,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR val token = sanitizer["access_token"]!! val expiresIn = sanitizer["expires_in"]!! @@ -87,7 +87,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { val data = searchShows(name) ?: return null - return data.data?.Page?.media?.map { + return data.data?.page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, this.name, @@ -101,7 +101,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getResult(id: String): SyncAPI.SyncResult { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") - val season = getSeason(internalId).data.Media + val season = getSeason(internalId).data.media return SyncAPI.SyncResult( season.id.toString(), @@ -301,12 +301,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { //println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") val shows = searchShows(name.replace(blackListRegex, "")) - shows?.data?.Page?.media?.find { + shows?.data?.page?.media?.find { (malId ?: "NONE") == it.idMal.toString() }?.let { return it } val filtered = - shows?.data?.Page?.media?.filter { + shows?.data?.page?.media?.filter { (((it.startDate.year ?: year.toString()) == year.toString() || year == null)) } @@ -496,7 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q, true) val d = parseJson(data ?: return null) - val main = d.data?.Media + val main = d.data?.media if (main?.mediaListEntry != null) { return AniListTitleHolder( title = main.title, @@ -536,7 +536,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { headers = mapOf( "Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null), - if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" + if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache" ), cacheTime = 0, data = mapOf( @@ -647,7 +647,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class Data( - @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection + @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection ) private fun getAniListListCached(): Array? { @@ -659,7 +659,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { if (checkToken()) return null return if (requireLibraryRefresh) { - val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() + val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray() if (list != null) { setKey(ANILIST_CACHED_LIST, list) } @@ -678,7 +678,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { // To fill empty lists when AniList does not return them val baseMap = - AniListStatusType.values().filter { it.value >= 0 }.associate { + AniListStatusType.entries.filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -764,7 +764,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { /** Used to query a saved MediaItem on the list to get the id for removal */ data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null) - data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null) + data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null) data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( @@ -787,7 +787,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { """ val response = postApi(idQuery) val listId = - tryParseJson(response)?.data?.MediaList?.id ?: return false + tryParseJson(response)?.data?.mediaList?.id ?: return false """ mutation(${'$'}id: Int = $listId) { DeleteMediaListEntry(id: ${'$'}id) { @@ -836,7 +836,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q) if (data.isNullOrBlank()) return null val userData = parseJson(data) - val u = userData.data?.Viewer + val u = userData.data?.viewer val user = AniListUser( u?.id, u?.name, @@ -858,8 +858,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { suspend fun getSeasonRecursive(id: Int) { val season = getSeason(id) seasons.add(season) - if (season.data.Media.format?.startsWith("TV") == true) { - season.data.Media.relations?.edges?.forEach { + if (season.data.media.format?.startsWith("TV") == true) { + season.data.media.relations?.edges?.forEach { if (it.node?.format != null) { if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) { getSeasonRecursive(it.node.id) @@ -878,7 +878,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class SeasonData( - @JsonProperty("Media") val Media: SeasonMedia, + @JsonProperty("Media") val media: SeasonMedia, ) data class SeasonMedia( @@ -1050,7 +1050,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class AniListData( - @JsonProperty("Viewer") val Viewer: AniListViewer?, + @JsonProperty("Viewer") val viewer: AniListViewer?, ) data class AniListRoot( @@ -1090,7 +1090,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class LikeData( - @JsonProperty("Viewer") val Viewer: LikeViewer?, + @JsonProperty("Viewer") val viewer: LikeViewer?, ) data class LikeRoot( @@ -1130,7 +1130,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetDataData( - @JsonProperty("Media") val Media: GetDataMedia?, + @JsonProperty("Media") val media: GetDataMedia?, ) data class GetDataRoot( @@ -1163,7 +1163,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetSearchPage( - @JsonProperty("Page") val Page: GetSearchData?, + @JsonProperty("Page") val page: GetSearchData?, ) data class GetSearchData( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index 00f8d00c..f819cd3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -119,8 +119,6 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, -// ListSorting.RatingHigh, -// ListSorting.RatingLow, ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 24ef7136..6046a0f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -19,14 +19,18 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import java.net.URL import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat -import java.util.* +import java.time.Instant +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone /** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */ const val MAL_MAX_SEARCH_LIMIT = 25 @@ -51,7 +55,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } override fun loginInfo(): AuthAPI.LoginInfo? { - //getMalUser(true)? getKey(accountId, MAL_USER_KEY)?.let { user -> return AuthAPI.LoginInfo( profilePicture = user.picture, @@ -84,7 +87,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { this.name, node.id.toString(), "$mainUrl/anime/${node.id}/", - node.main_picture?.large ?: node.main_picture?.medium + node.mainPicture?.large ?: node.mainPicture?.medium ) } } @@ -178,7 +181,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDate(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time } catch (e: Exception) { null } @@ -190,7 +193,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { apiName = this.name, syncId = node.id.toString(), url = "$mainUrl/anime/${node.id}", - posterUrl = node.main_picture?.large + posterUrl = node.mainPicture?.large ) } @@ -244,12 +247,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val internalId = id.toIntOrNull() ?: return null val data = - getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status") + getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status") return SyncAPI.SyncStatus( score = data?.score, - status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) , + status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)), isFavorite = null, - watchedEpisodes = data?.num_episodes_watched, + watchedEpisodes = data?.numEpisodesWatched, ) } @@ -291,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) } catch (e: Exception) { @@ -302,7 +305,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR val state = sanitizer["state"]!! if (state == "RequestID$requestId") { val currentCode = sanitizer["code"]!! @@ -351,9 +354,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { try { if (response != "") { val token = parseJson(response) - setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) - setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) - setKey(accountId, MAL_TOKEN_KEY, token.access_token) + setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime)) + setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken) + setKey(accountId, MAL_TOKEN_KEY, token.accessToken) requireLibraryRefresh = true } } catch (e: Exception) { @@ -395,53 +398,53 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class Node( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String, - @JsonProperty("main_picture") val main_picture: MainPicture?, - @JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?, - @JsonProperty("media_type") val media_type: String?, - @JsonProperty("num_episodes") val num_episodes: Int?, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("num_episodes") val numEpisodes: Int?, @JsonProperty("status") val status: String?, - @JsonProperty("start_date") val start_date: String?, - @JsonProperty("end_date") val end_date: String?, - @JsonProperty("average_episode_duration") val average_episode_duration: Int?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, @JsonProperty("synopsis") val synopsis: String?, @JsonProperty("mean") val mean: Double?, @JsonProperty("genres") val genres: List?, @JsonProperty("rank") val rank: Int?, @JsonProperty("popularity") val popularity: Int?, - @JsonProperty("num_list_users") val num_list_users: Int?, - @JsonProperty("num_favorites") val num_favorites: Int?, - @JsonProperty("num_scoring_users") val num_scoring_users: Int?, - @JsonProperty("start_season") val start_season: StartSeason?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_favorites") val numFavorites: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, @JsonProperty("broadcast") val broadcast: Broadcast?, @JsonProperty("nsfw") val nsfw: String?, - @JsonProperty("created_at") val created_at: String?, - @JsonProperty("updated_at") val updated_at: String? + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String? ) data class ListStatus( @JsonProperty("status") val status: String?, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, - @JsonProperty("is_rewatching") val is_rewatching: Boolean, - @JsonProperty("updated_at") val updated_at: String, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) data class Data( @JsonProperty("node") val node: Node, - @JsonProperty("list_status") val list_status: ListStatus?, + @JsonProperty("list_status") val listStatus: ListStatus?, ) { fun toLibraryItem(): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( this.node.title, "https://myanimelist.net/anime/${this.node.id}/", this.node.id.toString(), - this.list_status?.num_episodes_watched, - this.node.num_episodes, - this.list_status?.score?.times(10), - parseDateLong(this.list_status?.updated_at), + this.listStatus?.numEpisodesWatched, + this.node.numEpisodes, + this.listStatus?.score?.times(10), + parseDateLong(this.listStatus?.updatedAt), "MAL", TvType.Anime, - this.node.main_picture?.large ?: this.node.main_picture?.medium, + this.node.mainPicture?.large ?: this.node.mainPicture?.medium, null, null, plot = this.node.synopsis, @@ -470,8 +473,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ) data class Broadcast( - @JsonProperty("day_of_the_week") val day_of_the_week: String?, - @JsonProperty("start_time") val start_time: String? + @JsonProperty("day_of_the_week") val dayOfTheWeek: String?, + @JsonProperty("start_time") val startTime: String? ) private fun getMalAnimeListCached(): Array? { @@ -491,14 +494,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { val list = getMalAnimeListSmart()?.groupBy { - convertToStatus(it.list_status?.status ?: "").stringRes + convertToStatus(it.listStatus?.status ?: "").stringRes }?.mapValues { group -> group.value.map { it.toLibraryItem() } } ?: emptyMap() // To fill empty lists when MAL does not return them val baseMap = - MalStatusType.values().filter { it.value >= 0 }.associate { + MalStatusType.entries.filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -573,7 +576,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ).text val values = parseJson(res) val titles = - values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } + values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) } for (t in titles) { allTitles[t.id] = t } @@ -582,11 +585,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } - fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { + private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { // No time remaining if the show has already ended try { endDate?.let { - if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null + if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it) + ?.before(Date.from(Instant.now())) != false + ) return@convertJapanTimeToTimeRemaining null } } catch (e: ParseException) { logError(e) @@ -603,7 +608,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH) val currentYear = currentDate.get(Calendar.YEAR) - val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") + val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault()) dateFormat.timeZone = TimeZone.getTimeZone("Japan") val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null @@ -647,13 +652,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { id: Int, status: MalStatusType? = null, score: Int? = null, - num_watched_episodes: Int? = null, + numWatchedEpisodes: Int? = null, ): Boolean { val res = setScoreRequest( id, if (status == null) null else malStatusAsString[maxOf(0, status.value)], score, - num_watched_episodes + numWatchedEpisodes ) return if (res.isNullOrBlank()) { @@ -670,17 +675,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } + @Suppress("UNCHECKED_CAST") private suspend fun setScoreRequest( id: Int, status: String? = null, score: Int? = null, - num_watched_episodes: Int? = null, + numWatchedEpisodes: Int? = null, ): String? { val data = mapOf( "status" to status, "score" to score?.toString(), - "num_watched_episodes" to num_watched_episodes?.toString() - ).filter { it.value != null } as Map + "num_watched_episodes" to numWatchedEpisodes?.toString() + ).filterValues { it != null } as Map return app.put( "$apiUrl/v2/anime/$id/my_list_status", @@ -693,10 +699,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class ResponseToken( - @JsonProperty("token_type") val token_type: String, - @JsonProperty("expires_in") val expires_in: Int, - @JsonProperty("access_token") val access_token: String, - @JsonProperty("refresh_token") val refresh_token: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, ) data class MalRoot( @@ -705,7 +711,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalDatum( @JsonProperty("node") val node: MalNode, - @JsonProperty("list_status") val list_status: MalStatus, + @JsonProperty("list_status") val listStatus: MalStatus, ) data class MalNode( @@ -722,16 +728,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalStatus( @JsonProperty("status") val status: String, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, - @JsonProperty("is_rewatching") val is_rewatching: Boolean, - @JsonProperty("updated_at") val updated_at: String, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) data class MalUser( @JsonProperty("id") val id: Int, @JsonProperty("name") val name: String, @JsonProperty("location") val location: String, - @JsonProperty("joined_at") val joined_at: String, + @JsonProperty("joined_at") val joinedAt: String, @JsonProperty("picture") val picture: String?, ) @@ -744,9 +750,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class SmallMalAnime( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String?, - @JsonProperty("num_episodes") val num_episodes: Int, - @JsonProperty("my_list_status") val my_list_status: MalStatus?, - @JsonProperty("main_picture") val main_picture: MalMainPicture?, + @JsonProperty("num_episodes") val numEpisodes: Int, + @JsonProperty("my_list_status") val myListStatus: MalStatus?, + @JsonProperty("main_picture") val mainPicture: MalMainPicture?, ) data class MalSearchNode( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 6412ff1b..37b95614 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager -import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.AppUtils import okhttp3.Interceptor import okhttp3.Response @@ -30,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi companion object { const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile - const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" - const val host = "https://api.opensubtitles.com/api/v1" + const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" + const val HOST = "https://api.opensubtitles.com/api/v1" const val TAG = "OPENSUBS" - const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms + const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms var currentCoolDown: Long = 0L var currentSession: SubtitleOAuthEntity? = null } @@ -49,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi chain.request().newBuilder() .removeHeader("user-agent") .addHeader("user-agent", userAgent) - .addHeader("Api-Key", apiKey) + .addHeader("Api-Key", API_KEY) .build() ) } @@ -66,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi } private fun throwGotTooManyRequests() { - currentCoolDown = unixTimeMs + coolDownDuration + currentCoolDown = unixTimeMs + COOLDOWN_DURATION throw ErrorLoadingException("Too many requests") } @@ -115,7 +114,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi private suspend fun initLogin(username: String, password: String): Boolean { //Log.i(TAG, "DATA = [$username] [$password]") val response = app.post( - url = "$host/login", + url = "$HOST/login", headers = mapOf( "Content-Type" to "application/json", ), @@ -134,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi SubtitleOAuthEntity( user = username, pass = password, - access_token = token.token ?: run { + accessToken = token.token ?: run { return false }) ) @@ -197,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val searchQueryUrl = when (imdbId > 0) { //Use imdb_id to search if its valid - true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" - false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" } val req = app.get( @@ -233,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val year = featureDetails?.year ?: query.year val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie - val isHearingImpaired = attr.hearing_impaired ?: false + val isHearingImpaired = attr.hearingImpaired ?: false //Log.i(TAG, "Result id/name => ${item.id} / $name") item.attributes?.files?.forEach { file -> val resultData = file.fileId?.toString() ?: "" @@ -266,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val req = app.post( - url = "$host/download", + url = "$HOST/download", headers = mapOf( Pair( "Authorization", - "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" + "Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}" ), Pair("Content-Type", "application/json"), Pair("Accept", "*/*") @@ -299,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi data class SubtitleOAuthEntity( var user: String, var pass: String, - var access_token: String, + var accessToken: String, ) data class OAuthToken( @@ -324,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi @JsonProperty("url") var url: String? = null, @JsonProperty("files") var files: List? = listOf(), @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), - @JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null, + @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null, ) data class ResultFiles( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 27975d19..e5db626b 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 @@ -38,6 +38,7 @@ import java.security.SecureRandom import java.text.SimpleDateFormat import java.time.Instant import java.util.Date +import java.util.Locale import java.util.TimeZone import kotlin.time.Duration import kotlin.time.DurationUnit @@ -144,8 +145,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } companion object { - private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID - private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET + private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID + private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET private var lastLoginState = "" const val SIMKL_TOKEN_KEY: String = "simkl_token" @@ -154,10 +155,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" /** 2014-09-01T09:10:11Z -> 1409562611 */ - private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'" fun getUnixTime(string: String?): Long? { return try { - SimpleDateFormat(simklDateFormat).apply { + SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.parse( string ?: return null @@ -171,7 +172,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** 1409562611 -> 2014-09-01T09:10:11Z */ fun getDateTime(unixTime: Long?): String? { return try { - SimpleDateFormat(simklDateFormat).apply { + SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.format( Date.from( @@ -208,7 +209,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { companion object { fun fromString(string: String): SimklListStatusType? { - return SimklListStatusType.values().firstOrNull { + return SimklListStatusType.entries.firstOrNull { it.originalName == string } } @@ -219,17 +220,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonInclude(JsonInclude.Include.NON_EMPTY) data class TokenRequest( @JsonProperty("code") val code: String, - @JsonProperty("client_id") val client_id: String = clientId, - @JsonProperty("client_secret") val client_secret: String = clientSecret, - @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", - @JsonProperty("grant_type") val grant_type: String = "authorization_code" + @JsonProperty("client_id") val clientId: String = CLIENT_ID, + @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET, + @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", + @JsonProperty("grant_type") val grantType: String = "authorization_code" ) data class TokenResponse( /** No expiration date */ - val access_token: String, - val token_type: String, - val scope: String + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("scope") val scope: String ) // ------------------- @@ -261,15 +262,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { // ------------------- data class ActivitiesResponse( - val all: String?, - val tv_shows: UpdatedAt, - val anime: UpdatedAt, - val movies: UpdatedAt, + @JsonProperty("all") val all: String?, + @JsonProperty("tv_shows") val tvShows: UpdatedAt, + @JsonProperty("anime") val anime: UpdatedAt, + @JsonProperty("movies") val movies: UpdatedAt, ) { data class UpdatedAt( - val all: String?, - val removed_from_list: String?, - val rated_at: String?, + @JsonProperty("all") val all: String?, + @JsonProperty("removed_from_list") val removedFromList: String?, + @JsonProperty("rated_at") val ratedAt: String?, ) } @@ -308,7 +309,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("title") val title: String?, @JsonProperty("year") val year: Int?, @JsonProperty("ids") val ids: Ids?, - @JsonProperty("total_episodes") val total_episodes: Int? = null, + @JsonProperty("total_episodes") val totalEpisodes: Int? = null, @JsonProperty("status") val status: String? = null, @JsonProperty("poster") val poster: String? = null, @JsonProperty("type") val type: String? = null, @@ -540,7 +541,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } debugPrint { "Requesting episodes from $url" } - return app.get(url, params = mapOf("client_id" to clientId)) + return app.get(url, params = mapOf("client_id" to CLIENT_ID)) .parsedSafe>()?.also { val cacheTime = if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value @@ -558,7 +559,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("seasons") seasons: List? = null, @JsonProperty("episodes") episodes: List? = null, @JsonProperty("rating") val rating: Int? = null, - @JsonProperty("rated_at") val rated_at: String? = null, + @JsonProperty("rated_at") val ratedAt: String? = null, ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -567,7 +568,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("rating") val rating: Int, - @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime) + @JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -576,7 +577,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("to") val to: String, - @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) + @JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -631,24 +632,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } interface Metadata { - val last_watched_at: String? + val lastWatchedAt: String? val status: String? - val user_rating: Int? - val last_watched: String? - val watched_episodes_count: Int? - val total_episodes_count: Int? + val userRating: Int? + val lastWatched: String? + val watchedEpisodesCount: Int? + val totalEpisodesCount: Int? fun getIds(): ShowMetadata.Show.Ids fun toLibraryItem(): SyncAPI.LibraryItem } data class MovieMetadata( - override val last_watched_at: String?, - override val status: String, - override val user_rating: Int?, - override val last_watched: String?, - override val watched_episodes_count: Int?, - override val total_episodes_count: Int?, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, val movie: ShowMetadata.Show ) : Metadata { override fun getIds(): ShowMetadata.Show.Ids { @@ -660,10 +661,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.title, "https://simkl.com/tv/${movie.ids.simkl}", movie.ids.simkl.toString(), - this.watched_episodes_count, - this.total_episodes_count, - this.user_rating?.times(10), - getUnixTime(last_watched_at) ?: 0, + this.watchedEpisodesCount, + this.totalEpisodesCount, + this.userRating?.times(10), + getUnixTime(lastWatchedAt) ?: 0, "Simkl", TvType.Movie, this.movie.poster?.let { getPosterUrl(it) }, @@ -675,12 +676,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class ShowMetadata( - @JsonProperty("last_watched_at") override val last_watched_at: String?, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, @JsonProperty("status") override val status: String, - @JsonProperty("user_rating") override val user_rating: Int?, - @JsonProperty("last_watched") override val last_watched: String?, - @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, - @JsonProperty("total_episodes_count") override val total_episodes_count: Int?, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, @JsonProperty("show") val show: Show ) : Metadata { override fun getIds(): Show.Ids { @@ -692,10 +693,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.title, "https://simkl.com/tv/${show.ids.simkl}", show.ids.simkl.toString(), - this.watched_episodes_count, - this.total_episodes_count, - this.user_rating?.times(10), - getUnixTime(last_watched_at) ?: 0, + this.watchedEpisodesCount, + this.totalEpisodesCount, + this.userRating?.times(10), + getUnixTime(lastWatchedAt) ?: 0, "Simkl", TvType.Anime, this.show.poster?.let { getPosterUrl(it) }, @@ -749,7 +750,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { chain.request() .newBuilder() .addHeader("Authorization", "Bearer $token") - .addHeader("simkl-api-key", clientId) + .addHeader("simkl-api-key", CLIENT_ID) .build() ) } @@ -810,7 +811,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val episodeConstructor = SimklEpisodeConstructor( searchResult.ids?.simkl, searchResult.type, - searchResult.total_episodes, + searchResult.totalEpisodes, searchResult.hasEnded() ) @@ -832,12 +833,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } ?: return null, - score = foundItem.user_rating, - watchedEpisodes = foundItem.watched_episodes_count, - maxEpisodes = searchResult.total_episodes, + score = foundItem.userRating, + watchedEpisodes = foundItem.watchedEpisodesCount, + maxEpisodes = searchResult.totalEpisodes, episodeConstructor = episodeConstructor, - oldEpisodes = foundItem.watched_episodes_count ?: 0, - oldScore = foundItem.user_rating, + oldEpisodes = foundItem.watchedEpisodesCount ?: 0, + oldScore = foundItem.userRating, oldStatus = foundItem.status ) } else { @@ -845,7 +846,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { status = SyncWatchType.fromInternalId(SimklListStatusType.None.value), score = 0, watchedEpisodes = 0, - maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, + maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes, episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, @@ -891,12 +892,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ - suspend fun searchByIds(serviceMap: Map): Array? { + private suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() return app.get( "$mainUrl/search/id", - params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> + params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) -> service.originalName to id } ).parsedSafe() @@ -904,14 +905,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) + "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } override fun authenticate(activity: FragmentActivity?) { lastLoginState = BigInteger(130, SecureRandom()).toString(32) val url = - "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" + "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}&state=$lastLoginState" openBrowser(url, activity) } @@ -961,15 +962,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val activities = getActivities() val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME) val lastRemoval = listOf( - activities?.tv_shows?.removed_from_list, - activities?.anime?.removed_from_list, - activities?.movies?.removed_from_list + activities?.tvShows?.removedFromList, + activities?.anime?.removedFromList, + activities?.movies?.removedFromList ).maxOf { getUnixTime(it) ?: -1 } val lastRealUpdate = listOf( - activities?.tv_shows?.all, + activities?.tvShows?.all, activities?.anime?.all, activities?.movies?.all, ).maxOf { @@ -1039,7 +1040,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getDevicePin(): OAuth2API.PinAuthData? { val pinAuthResp = app.get( - "$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}" + "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}" ).parsedSafe() ?: return null return OAuth2API.PinAuthData( @@ -1053,7 +1054,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { val pinAuthResp = app.get( - "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId" + "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$CLIENT_ID" ).parsedSafe() ?: return false if (pinAuthResp.accessToken != null) { @@ -1088,7 +1089,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ).parsedSafe() ?: return false switchToNewAccount() - setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) + setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken) val user = getUser() if (user == null) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt index 0e233ece..8dad1f88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -59,6 +59,7 @@ class SubSourceApi : AbstractSubProvider { it?.subs?.filter { sub -> sub.releaseName!!.contains( String.format( + null, "E%02d", query.epNumber ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index a075cc2e..9150cfc5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) { private val cache = threadSafeListOf() private var cacheIndex: Int = 0 - const val cacheSize = 20 + const val CACHE_SIZE = 20 } private fun afterPluginsLoaded(forceReload: Boolean) { @@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) { val add = SavedLoadResponse(unixTime, response, lookingForHash) synchronized(cache) { - if (cache.size > cacheSize) { + if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache - cacheIndex = (cacheIndex + 1) % cacheSize + cacheIndex = (cacheIndex + 1) % CACHE_SIZE } else { cache.add(add) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt index d90177f5..e930961c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -112,6 +112,7 @@ abstract class BaseAdapter< holder.onViewDetachedFromWindow() } + @Suppress("UNCHECKED_CAST") fun save(recyclerView: RecyclerView) { for (child in recyclerView.children) { val holder = @@ -124,6 +125,7 @@ abstract class BaseAdapter< stateViewModel.layoutManagerStates[id]?.clear() } + @Suppress("UNCHECKED_CAST") private fun getState(holder: ViewHolderState): S? = stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 6bafa975..1eaac505 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -6,6 +6,7 @@ import android.view.Menu import android.view.View.* import android.widget.* import androidx.appcompat.app.AlertDialog +import androidx.media3.common.util.UnstableApi import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule @@ -263,6 +264,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi var isLoadingMore = false + override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() val meta = getCurrentMetaData() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 1a9549e1..78ad2a6b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -8,8 +8,8 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs -class GrdLayoutManager(val context: Context, _spanCount: Int) : - GridLayoutManager(context, _spanCount) { +class GrdLayoutManager(val context: Context, spanCount: Int) : + GridLayoutManager(context, spanCount) { override fun onFocusSearchFailed( focused: View, focusDirection: Int, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt index c7041776..4879d2e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt @@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() { FrameLayout.LayoutParams.WRAP_CONTENT) binding.frame.addView(newStar) - newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX + newStar.scaleX += Math.random().toFloat() * 1.5f newStar.scaleY = newStar.scaleX starW *= newStar.scaleX starH *= newStar.scaleY diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt index f721401e..12a5ae2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt @@ -15,7 +15,7 @@ open class NonFinalAdapterListUpdateCallback /** * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. * - * @param adapter The Adapter to send updates to. + * @param mAdapter The Adapter to send updates to. */(private var mAdapter: RecyclerView.Adapter<*>) : ListUpdateCallback { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt index 9532d1a9..b778ba5a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt @@ -13,7 +13,7 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24); companion object { - fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE } } @@ -36,6 +36,6 @@ enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @Dr REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24); companion object { - fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 15e66b38..5e2b97e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -8,8 +8,10 @@ import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.OptIn import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.media3.common.util.UnstableApi import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.USER_AGENT @@ -29,6 +31,7 @@ class WebviewFragment : Fragment() { } binding?.webView?.webViewClient = object : WebViewClient() { + @OptIn(UnstableApi::class) override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index f10e103e..45132131 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -54,6 +54,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } init { + @Suppress("LeakingThis") resetViewData() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index d97a4b88..20a44461 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -13,7 +13,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper class DownloadButton(context: Context, attributeSet: AttributeSet) : PieFetchButton(context, attributeSet) { - var mainText: TextView? = null + private var mainText: TextView? = null override fun onAttachedToWindow() { super.onAttachedToWindow() progressText = findViewById(R.id.result_movie_download_text_precentage) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index ebed901f..b25486eb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout +import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(view) { @@ -54,7 +54,7 @@ class HomeChildItemAdapter( var hasNext: Boolean = false override fun onCreateContent(parent: ViewGroup): ViewHolderState { - val expanded = parent.context.IsBottomLayout() + val expanded = parent.context.isBottomLayout() /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) @@ -133,7 +133,6 @@ class HomeChildItemAdapter( item, position, holder.itemView, - null, // nextFocusBehavior, nextFocusUp, nextFocusDown ) 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 82a92d80..49de2503 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 @@ -17,7 +17,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.* import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -234,7 +233,7 @@ class HomeFragment : Fragment() { return bottomSheetDialogBuilder } - fun getPairList( + private fun getPairList( anime: Chip?, cartoons: Chip?, tvs: Chip?, 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 916cb9ae..8bc0aa28 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 @@ -1,6 +1,8 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Build import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -53,12 +55,12 @@ open class ParentItemAdapter( "value", recyclerView?.layoutManager?.onSaveInstanceState() ) - (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) + (recyclerView?.adapter as? BaseAdapter<*, *>)?.save(recyclerView) } override fun restore(state: Bundle) { (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( - state.getParcelable("value") + state.getSafeParcelable("value") ) } } @@ -169,4 +171,9 @@ open class ParentItemAdapter( submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } .toMutableList()) } -} \ No newline at end of file +} + +@Suppress("DEPRECATION") +inline fun Bundle.getSafeParcelable(key: String): T? = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelable(key) + else getParcelable(key, T::class.java) \ No newline at end of file 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 2e98dd1f..339ef1e1 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 @@ -117,15 +117,12 @@ class HomeParentItemAdapterPreview( } override fun restore(state: Bundle) { - state.getParcelable("resumeRecyclerView")?.let { recycle -> + state.getSafeParcelable("resumeRecyclerView")?.let { recycle -> resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - state.getParcelable("bookmarkRecyclerView")?.let { recycle -> + state.getSafeParcelable("bookmarkRecyclerView")?.let { recycle -> bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - //state.getInt("previewViewpager").let { recycle -> - // previewViewpager.setCurrentItem(recycle,true) - //} } val previewAdapter = HomeScrollAdapter(fragment = fragment) 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 9e70d088..24ca4df2 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 @@ -152,7 +152,7 @@ class HomeViewModel : ViewModel() { } }?.distinctBy { it.first } ?: return@launchSafe - val length = WatchType.values().size + val length = WatchType.entries.size val currentWatchTypes = mutableSetOf() for (watch in watchStatusIds) { @@ -387,7 +387,9 @@ class HomeViewModel : ViewModel() { } is Resource.Failure -> { + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _page.postValue(data!!) + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _preview.postValue(data!!) } @@ -397,9 +399,7 @@ class HomeViewModel : ViewModel() { } fun click(callback: SearchClickCallback) { - if (callback.action == SEARCH_ACTION_FOCUSED) { - //focusCallback(callback.card) - } else { + if (callback.action != SEARCH_ACTION_FOCUSED) { SearchHelper.handleSearchClickCallback(callback) } } @@ -516,7 +516,7 @@ class HomeViewModel : ViewModel() { } else { _page.postValue(Resource.Loading()) if (preferredApiName != null) - _apiName.postValue(preferredApiName) + _apiName.postValue(preferredApiName!!) } } else { // if the api is found, then set it to it and save key 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 7144de09..5b240693 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 @@ -600,8 +600,4 @@ class LibraryFragment : Fragment() { } } -class MenuSearchView(context: Context) : SearchView(context) { - override fun onActionViewCollapsed() { - super.onActionViewCollapsed() - } -} \ No newline at end of file +class MenuSearchView(context: Context) : SearchView(context) \ 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 cfd22220..0110187f 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 @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.home.getSafeParcelable import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -32,7 +33,7 @@ class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) } override fun restore(state: Bundle) { - state.getParcelable("pageRecyclerview")?.let { recycle -> + state.getSafeParcelable("pageRecyclerview")?.let { recycle -> binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) } } 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 9d838c97..88c34c87 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 @@ -25,6 +25,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException +import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.ui.* @@ -216,7 +217,7 @@ abstract class AbstractPlayerFragment( return } player.handleEvent( - CSPlayerEvent.values()[intent.getIntExtra( + CSPlayerEvent.entries[intent.getIntExtra( EXTRA_CONTROL_TYPE, 0 )], source = PlayerEventSource.UI @@ -603,12 +604,12 @@ abstract class AbstractPlayerFragment( } fun nextResize() { - resizeMode = (resizeMode + 1) % PlayerResize.values().size + resizeMode = (resizeMode + 1) % PlayerResize.entries.size resize(resizeMode, true) } fun resize(resize: Int, showToast: Boolean) { - resize(PlayerResize.values()[resize], showToast) + resize(PlayerResize.entries[resize], showToast) } @SuppressLint("UnsafeOptInUsageError") 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 735e4095..86d67b28 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 @@ -9,7 +9,11 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -import androidx.media3.common.C.* +import androidx.annotation.OptIn +import androidx.media3.common.C.TIME_UNSET +import androidx.media3.common.C.TRACK_TYPE_AUDIO +import androidx.media3.common.C.TRACK_TYPE_TEXT +import androidx.media3.common.C.TRACK_TYPE_VIDEO import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes @@ -19,9 +23,10 @@ import androidx.media3.common.TrackGroup import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.VideoSize +import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.cache.CacheDataSource @@ -66,7 +71,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File -import java.lang.IllegalArgumentException import java.util.UUID import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext @@ -84,7 +88,7 @@ const val toleranceBeforeUs = 300_000L * seek position, in microseconds. Must be non-negative. */ const val toleranceAfterUs = 300_000L - +@OptIn(UnstableApi::class) class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null @@ -257,7 +261,6 @@ class CS3IPlayer : IPlayer { private var currentSubtitles: SubtitleData? = null - @SuppressLint("UnsafeOptInUsageError") private fun List.getTrack(id: String?): Pair? { if (id == null) return null // This beast of an expression does: @@ -342,7 +345,6 @@ class CS3IPlayer : IPlayer { }.flatten() } - @SuppressLint("UnsafeOptInUsageError") private fun Tracks.Group.getFormats(): List> { return (0 until this.mediaTrackGroup.length).mapNotNull { i -> if (this.isSupported) @@ -371,7 +373,6 @@ class CS3IPlayer : IPlayer { ) } - @SuppressLint("UnsafeOptInUsageError") override fun getVideoTracks(): CurrentTracks { val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } @@ -391,7 +392,6 @@ class CS3IPlayer : IPlayer { /** * @return True if the player should be reloaded * */ - @SuppressLint("UnsafeOptInUsageError") override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle @@ -451,7 +451,7 @@ class CS3IPlayer : IPlayer { } ?: false } - var currentSubtitleOffset: Long = 0 + private var currentSubtitleOffset: Long = 0 override fun setSubtitleOffset(offset: Long) { currentSubtitleOffset = offset @@ -459,7 +459,7 @@ class CS3IPlayer : IPlayer { } override fun getSubtitleOffset(): Long { - return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset + return currentSubtitleOffset } override fun getCurrentPreferredSubtitle(): SubtitleData? { @@ -470,7 +470,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") override fun getAspectRatio(): Rational? { return exoPlayer?.videoFormat?.let { format -> Rational(format.width, format.height) @@ -481,14 +480,13 @@ class CS3IPlayer : IPlayer { subtitleHelper.setSubStyle(style) } - @SuppressLint("UnsafeOptInUsageError") override fun saveData() { Log.i(TAG, "saveData") updatedTime() exoPlayer?.let { exo -> playbackPosition = exo.currentPosition - currentWindow = exo.currentWindowIndex + currentWindow = exo.currentMediaItemIndex isPlaying = exo.isPlaying } } @@ -500,7 +498,7 @@ class CS3IPlayer : IPlayer { updatedTime() exoPlayer?.apply { - setPlayWhenReady(false) + playWhenReady = false stop() release() } @@ -563,7 +561,6 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null - @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(headers: Map): HttpDataSource.Factory { val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) return source.apply { @@ -571,7 +568,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { val provider = getApiFromNameNull(link.source) val interceptor = provider?.getVideoInterceptor(link) @@ -604,53 +600,10 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun Context.createOfflineSource(): DataSource.Factory { - return DefaultDataSourceFactory(this, USER_AGENT) + return DefaultDataSource.Factory(this, DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT)) } - /*private fun getSubSources( - onlineSourceFactory: DataSource.Factory?, - offlineSourceFactory: DataSource.Factory?, - subHelper: PlayerSubtitleHelper, - ): Pair, List> { - val activeSubtitles = ArrayList() - val subSources = subHelper.getAllSubtitles().mapNotNull { sub -> - val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) - .setMimeType(sub.mimeType) - .setLanguage("_${sub.name}") - .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build() - when (sub.origin) { - SubtitleOrigin.DOWNLOADED_FILE -> { - if (offlineSourceFactory != null) { - activeSubtitles.add(sub) - SingleSampleMediaSource.Factory(offlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) - } else { - null - } - } - SubtitleOrigin.URL -> { - if (onlineSourceFactory != null) { - activeSubtitles.add(sub) - SingleSampleMediaSource.Factory(onlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) - } else { - null - } - } - SubtitleOrigin.OPEN_SUBTITLES -> { - // TODO - throw NotImplementedError() - } - } - } - println("SUBSRC: ${subSources.size} activeSubtitles : ${activeSubtitles.size} of ${subHelper.getAllSubtitles().size} ") - return Pair(subSources, activeSubtitles) - }*/ - - @SuppressLint("UnsafeOptInUsageError") private fun getCache(context: Context, cacheSize: Long): SimpleCache? { return try { val databaseProvider = StandaloneDatabaseProvider(context) @@ -682,7 +635,6 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } - @SuppressLint("UnsafeOptInUsageError") private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = trackSelector.buildUponParameters() @@ -696,7 +648,6 @@ class CS3IPlayer : IPlayer { var currentTextRenderer: CustomTextRenderer? = null - @SuppressLint("UnsafeOptInUsageError") private fun buildExoPlayer( context: Context, mediaItemSlices: List, @@ -736,7 +687,7 @@ class CS3IPlayer : IPlayer { textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ).also { this.currentTextRenderer = it } + ).also { renderer -> this.currentTextRenderer = renderer } currentTextRenderer } else it }.toTypedArray() @@ -1033,7 +984,7 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") + //fixme: Use onPlaybackStateChanged(int) and onPlayWhenReadyChanged(boolean, int) instead. override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { exoPlayer?.let { exo -> event( @@ -1169,7 +1120,6 @@ class CS3IPlayer : IPlayer { private var lastTimeStamps: List = emptyList() - @SuppressLint("UnsafeOptInUsageError") override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> @@ -1187,7 +1137,6 @@ class CS3IPlayer : IPlayer { updatedTime(source = PlayerEventSource.Player) } - @SuppressLint("UnsafeOptInUsageError") fun onRenderFirst() { if (hasUsedFirstRender) { // this insures that we only call this once per player load return @@ -1254,7 +1203,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun getSubSources( onlineSourceFactory: HttpDataSource.Factory?, offlineSourceFactory: DataSource.Factory?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index 20d093a6..07ce413e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context import android.util.Log +import androidx.annotation.OptIn import androidx.preference.PreferenceManager import androidx.media3.common.Format import androidx.media3.common.MimeTypes @@ -31,7 +32,7 @@ import java.nio.charset.Charset * @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not * enough to identify the subtitle format. **/ -@UnstableApi +@OptIn(UnstableApi::class) class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { companion object { fun updateForcedEncoding(context: Context) { @@ -72,7 +73,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { RegexOption.IGNORE_CASE ), ) - val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*""")) + val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\s]*?[])}]\s*""")) //https://emptycharacter.com/ //https://www.fileformat.info/info/unicode/char/200b/index.htm @@ -262,7 +263,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { } /** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */ -@UnstableApi +@OptIn(UnstableApi::class) class CustomSubtitleDecoderFactory : SubtitleDecoderFactory { override fun supportsFormat(format: Format): Boolean { // return SubtitleDecoderFactory.DEFAULT.supportsFormat(format) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt index d6b0735d..f2b863fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt @@ -1,11 +1,12 @@ package com.lagradost.cloudstream3.ui.player import android.os.Looper +import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.text.SubtitleDecoderFactory import androidx.media3.exoplayer.text.TextOutput -@UnstableApi +@OptIn(UnstableApi::class) class CustomTextRenderer( offset: Long, output: TextOutput?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 3b242172..a8a3106a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -49,7 +49,7 @@ class DownloadFileGenerator( return null } - fun cleanDisplayName(name: String): String { + private fun cleanDisplayName(name: String): String { return name.substringBeforeLast('.').trim() } 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 92ef279d..4279b542 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 @@ -8,14 +8,10 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.safefile.SafeFile import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri -const val DTAG = "PlayerActivity" - class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" 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 ef7d6bc1..b2e80749 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 @@ -25,6 +25,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import androidx.annotation.OptIn import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue @@ -35,6 +36,7 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.media3.common.util.UnstableApi import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener @@ -50,7 +52,6 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid 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.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -245,6 +246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { fadeAnimation.duration = 100 fadeAnimation.fillAfter = true + @OptIn(UnstableApi::class) val sView = subView val sStyle = subStyle if (sView != null && sStyle != null) { @@ -300,42 +302,40 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun restoreOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 - when (currentOrientation) { + val orientation = when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE Configuration.ORIENTATION_PORTRAIT -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + + else -> dynamicOrientation() } activity.requestedOrientation = orientation } private fun toggleOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 - when (currentOrientation) { + val orientation: Int = when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT - - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT Configuration.ORIENTATION_PORTRAIT -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + + else -> dynamicOrientation() } activity.requestedOrientation = orientation } open fun lockOrientation(activity: Activity) { - val display = + @Suppress("DEPRECATION") + val display = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + else activity.display!! val rotation = display.rotation val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 + val orientation: Int when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> orientation = @@ -344,15 +344,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() { else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() - Configuration.ORIENTATION_PORTRAIT -> orientation = if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + + else -> orientation = dynamicOrientation() } activity.requestedOrientation = orientation } @@ -1167,6 +1166,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { return true } + @SuppressLint("GestureBackNavigation") private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean { if (hasNavigated) { autoHide() @@ -1581,7 +1581,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } // cs3 is peak media center - setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV)) + setRemainingTimeCounter(durationMode || isLayout(TV)) playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ -> updateRemainingTime() } 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 1f7cc5bd..8e8f6bf5 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 @@ -6,6 +6,7 @@ import android.app.Dialog import android.content.Context import android.content.Intent import android.content.res.ColorStateList +import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -13,6 +14,7 @@ import android.view.View import android.view.ViewGroup import android.widget.* import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.OptIn import androidx.core.animation.addListener import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -21,6 +23,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -63,6 +66,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job +import java.io.Serializable import java.util.* import kotlin.math.abs @@ -234,7 +238,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun closestQuality(target: Int?): Qualities { if (target == null) return Qualities.Unknown - return Qualities.values().minBy { abs(it.value - target) } + return Qualities.entries.minBy { abs(it.value - target) } } private fun getLinkPriority( @@ -367,8 +371,6 @@ class GeneratorPlayer : FullScreenPlayer() { binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE binding.subtitleAdapter.adapter = arrayAdapter - val adapter = - binding.subtitleAdapter.adapter as? ArrayAdapter binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ -> currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener @@ -379,8 +381,8 @@ class GeneratorPlayer : FullScreenPlayer() { fun setSubtitlesList(list: List) { currentSubtitles = list - adapter?.clear() - adapter?.addAll(currentSubtitles) + arrayAdapter.clear() + arrayAdapter.addAll(currentSubtitles) } val currentTempMeta = getMetaData() @@ -522,7 +524,7 @@ class GeneratorPlayer : FullScreenPlayer() { //TODO: Set year text from currently loaded movie on Player //dialog.subtitles_search_year?.setText(currentTempMeta.year) } - + @OptIn(UnstableApi::class) private fun openSubPicker() { try { subsPathPicker.launch( @@ -795,7 +797,6 @@ class GeneratorPlayer : FullScreenPlayer() { settingsManager.edit().putString( ctx.getString(R.string.subtitles_encoding_key), prefValues[it] ).apply() - updateForcedEncoding(ctx) dismiss() player.seekTime(-1) // to update subtitles, a dirty trick @@ -1290,7 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun unwrapBundle(savedInstanceState: Bundle?) { Log.i(TAG, "unwrapBundle = $savedInstanceState") savedInstanceState?.let { bundle -> - sync.addSyncs(bundle.getSerializable("syncData") as? HashMap?) + sync.addSyncs(bundle.getSafeSerializable>("syncData")) } } @@ -1507,3 +1508,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } } + +@Suppress("DEPRECATION") +inline fun Bundle.getSafeSerializable(key: String) : T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSerializable(key) as? T else getSerializable(key, T::class.java) \ No newline at end of file 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 5f7161f7..89c6f73b 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 @@ -8,7 +8,6 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink enum class PlayerEventType(val value: Int) { - //Stop(-1), Pause(0), Play(1), SeekForward(2), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 89e3c8de..07ea56dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -4,7 +4,6 @@ import android.net.Uri import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java index 3482f21c..232440cc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -29,6 +29,7 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.text.Cue; @@ -66,7 +67,7 @@ import java.util.stream.Collectors; * obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s * is delegated to a {@link TextOutput}. */ -@UnstableApi +@OptIn(markerClass = UnstableApi.class) public class NonFinalTextRenderer extends BaseRenderer implements Callback { private static final String TAG = "TextRenderer"; @@ -74,7 +75,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { /** * @param trackType The track type that the renderer handles. One of the {@link C} {@code * TRACK_TYPE_*} constants. - * @param outputHandler + * @param outputHandler todo description */ public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) { super(trackType); @@ -416,13 +417,11 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE_OUTPUT: - invokeUpdateOutputInternal((List) msg.obj); - return true; - default: - throw new IllegalStateException(); + if (msg.what == MSG_UPDATE_OUTPUT) { + invokeUpdateOutputInternal((List) msg.obj); + return true; } + throw new IllegalStateException(); } private void invokeUpdateOutputInternal(List cues) { @@ -441,7 +440,6 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { } ).collect(Collectors.toList()); - output.onCues(fixedCues); output.onCues(new CueGroup(fixedCues, 0L)); } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index e6de1266..f00f8a61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -4,8 +4,8 @@ import android.app.Activity import android.content.ContentUris import android.net.Uri import androidx.core.content.ContextCompat.getString +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 1ba5a29f..122eaa97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { - val TAG = "PlayViewGen" + const val TAG = "PlayViewGen" } private var generator: IGenerator? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index 25d7e3dd..02a7ee03 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -4,7 +4,9 @@ import android.util.Log import android.util.TypedValue import android.view.ViewGroup import android.widget.FrameLayout +import androidx.annotation.OptIn import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat @@ -47,6 +49,7 @@ data class SubtitleData( } } +@OptIn(UnstableApi::class) class PlayerSubtitleHelper { private var activeSubtitles: Set = emptySet() private var allSubtitles: Set = emptySet() 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 index 7c78ce63..2d1feaab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -239,7 +239,11 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG // generated images 1:1 to idx of hsl private var images: Array = arrayOf() - private val TAG = "PreviewImgM3u8" + companion object { + private const 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 @@ -388,13 +392,6 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG 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)*/ } } @@ -412,14 +409,16 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe null } + companion object { + private const val TAG = "PreviewImgMp4" + } + 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) { @@ -524,7 +523,7 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe 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); + val img = retriever.image(frame.toLong(), params) if (!scope.isActive) return if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 90bd1ca7..6943c641 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.util.Log +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.LoadResponse diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index 1e2c9f67..ce457740 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -17,7 +17,6 @@ class PriorityAdapter(override val items: MutableList>) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), - //LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) ) } @@ -31,10 +30,6 @@ class PriorityAdapter(override val items: MutableList>) : val binding: PlayerPrioritizeItemBinding, ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: SourcePriority) { - /* val plusButton: ImageView = itemView.add_button - val subtractButton: ImageView = itemView.subtract_button - val priorityText: TextView = itemView.priority_text - val priorityNumber: TextView = itemView.priority_number*/ binding.priorityText.text = item.name fun updatePriority() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index b587276f..45f6aa66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -29,8 +29,6 @@ class ProfilesAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProfilesViewHolder( PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) - //LayoutInflater.from(parent.context) - // .inflate(R.layout.player_quality_profile_item, parent, false) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index 96249db4..3267efd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.player.source_priority -import android.content.Context import androidx.annotation.StringRes import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -104,7 +103,7 @@ object QualityDataHelper { * Must under all circumstances at least return one profile **/ fun getProfiles(): List { - val availableTypes = QualityProfileType.values().toMutableList() + val availableTypes = QualityProfileType.entries.toMutableList() val profiles = (1..PROFILE_COUNT).map { profileNumber -> // Get the real type val type = getQualityProfileType(profileNumber) @@ -140,12 +139,12 @@ object QualityDataHelper { } } - QualityProfileType.values().forEach { + QualityProfileType.entries.forEach { if (it.unique) insertType(profiles, it) } debugAssert({ - !QualityProfileType.values().all { type -> + !QualityProfileType.entries.all { type -> !type.unique || profiles.any { it.type == type } } }, { "All unique quality types do not exist" }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt index e3629158..0537092c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -65,7 +65,7 @@ class QualityProfileDialog( setDefaultBtt.setOnClickListener { val currentProfile = getCurrentProfile() ?: return@setOnClickListener - val choices = QualityDataHelper.QualityProfileType.values() + val choices = QualityDataHelper.QualityProfileType.entries .filter { it != QualityDataHelper.QualityProfileType.None } val choiceNames = choices.map { txt(it.stringRes).asString(context) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index 1b59882e..bc6282af 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -47,7 +47,7 @@ class SourcePriorityDialog( ) qualitiesRecyclerView.adapter = PriorityAdapter( - Qualities.values().mapNotNull { + Qualities.entries.mapNotNull { SourcePriority( it, Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 61188905..0ca326dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -3,8 +3,6 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.IdRes -import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -70,8 +68,7 @@ class ActorAdaptor( } } - private inner class CardViewHolder - constructor( + private inner class CardViewHolder( val binding: CastItemBinding, private val focusCallback: (View?) -> Unit = {} ) : diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 06be6bd5..d12521b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -169,8 +169,7 @@ class EpisodeAdapter( return cardList.size } - class EpisodeCardViewHolderLarge - constructor( + class EpisodeCardViewHolderLarge( val binding: ResultEpisodeLargeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, @@ -335,8 +334,7 @@ class EpisodeAdapter( } } - class EpisodeCardViewHolderSmall - constructor( + class EpisodeCardViewHolderSmall( val binding: ResultEpisodeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index 7b7bae43..eecd6262 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -8,18 +8,6 @@ import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -/* -class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter(context, resource) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val newConvertView = convertView ?: run { - val mInflater = context - .getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater - mInflater.inflate(resource, null) - } - getItem(position)?.let { (newConvertView as? ImageView?)?.setImageResource(it) } - return newConvertView - } -}*/ const val IMAGE_CLICK = 0 const val IMAGE_LONG_CLICK = 1 @@ -66,8 +54,7 @@ class ImageAdapter( diffResult.dispatchUpdatesTo(this) } - class ImageViewHolder - constructor(val binding: ResultMiniImageBinding) : + class ImageViewHolder(val binding: ResultMiniImageBinding) : RecyclerView.ViewHolder(binding.root) { fun bind( img: Int, 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 2f297098..f1399e8d 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 @@ -78,11 +78,12 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper open class ResultFragmentPhone : FullScreenPlayer() { - private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { - override fun onGestureRegionsUpdate(gestureRegions: List) { - binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + private val gestureRegionsListener = + object : PanelsChildGestureRegionObserver.GestureRegionsListener { + override fun onGestureRegionsUpdate(gestureRegions: List) { + binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + } } - } protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -336,7 +337,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } - // ===== ===== ===== resultBinding?.apply { @@ -430,16 +430,16 @@ open class ResultFragmentPhone : FullScreenPlayer() { 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 - } + // 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) ?: "" - showToast(txt(message, name), Toast.LENGTH_SHORT) + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + showToast(txt(message, name), Toast.LENGTH_SHORT) } context?.let { openBatteryOptimizationSettings(it) } } @@ -473,8 +473,16 @@ open class ResultFragmentPhone : FullScreenPlayer() { if (act.isCastApiAvailable()) { try { CastButtonFactory.setUpMediaRouteButton(act, this) - val castContext = CastContext.getSharedInstance(act.applicationContext) - isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE + CastContext.getSharedInstance(act.applicationContext) { + it.run() + }.addOnCompleteListener { + isGone = if (it.isSuccessful) { + it.result.castState == CastState.NO_DEVICES_AVAILABLE + } else { + true + } + + } // this shit leaks for some reason //castContext.addCastStateListener { state -> // media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE @@ -961,12 +969,12 @@ open class ResultFragmentPhone : FullScreenPlayer() { setOnClickListener { fab -> activity?.showBottomDialog( - WatchType.values().map { fab.context.getString(it.stringRes) }.toList(), + WatchType.entries.map { fab.context.getString(it.stringRes) }.toList(), watchType.ordinal, fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it], context) + viewModel.updateWatchStatus(WatchType.entries[it], context) } } } @@ -1046,7 +1054,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { text?.asStringNull(ctx) ?: return@mapNotNull null ) }) { - viewModel.changeDubStatus(DubStatus.values()[itemId]) + viewModel.changeDubStatus(DubStatus.entries[itemId]) } } } @@ -1103,7 +1111,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { override fun onPause() { super.onPause() - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) + PanelsChildGestureRegionObserver.Provider.get() + .addGestureRegionsUpdateListener(gestureRegionsListener) } private fun setRecommendations(rec: List?, validApiName: String?) { 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 a0207060..1878f0b8 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 @@ -56,7 +56,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage class ResultFragmentTv : Fragment() { - protected lateinit var viewModel: ResultViewModel2 + private lateinit var viewModel: ResultViewModel2 private var binding: FragmentResultTvBinding? = null override fun onDestroyView() { @@ -418,10 +418,6 @@ class ResultFragmentTv : Fragment() { resultCastItems.layoutManager = object : LinearListLayout(view.context) { - override fun onInterceptFocusSearch(focused: View, direction: Int): View? { - return super.onInterceptFocusSearch(focused, direction) - } - override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, @@ -649,7 +645,7 @@ class ResultFragmentTv : Fragment() { binding?.apply { - (data as? Resource.Success)?.value?.let { (text, ep) -> + (data as? Resource.Success)?.value?.let { (_, ep) -> resultPlayMovieButton.setOnClickListener { viewModel.handleAction( @@ -817,45 +813,8 @@ class ResultFragmentTv : Fragment() { } } - /* - * Okay so what is this fuckery? - * Basically Android TV will crash if you request a new focus while - * the adapter gets updated. - * - * This means that if you load thumbnails and request a next focus at the same time - * the app will crash without any way to catch it! - * - * How to bypass this? - * This code basically steals the focus for 500ms and puts it in an inescapable view - * then lets out the focus by requesting focus to result_episodes - */ - - val hasEpisodes = - !(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty() - /*val focus = activity?.currentFocus - - if (hasEpisodes) { - // Make it impossible to focus anywhere else! - temporaryNoFocus.isFocusable = true - temporaryNoFocus.requestFocus() - }*/ (resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value) - - /* if (hasEpisodes) main { - - delay(500) - // This might make some people sad as it changes the focus when leaving an episode :( - if(focus?.requestFocus() == true) { - temporaryNoFocus.isFocusable = false - return@main - } - temporaryNoFocus.isFocusable = false - temporaryNoFocus.requestFocus() - } - - if (hasNoFocus()) - binding?.resultEpisodes?.requestFocus()*/ } } } 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 ce0fbdc5..6443a923 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 @@ -2723,7 +2723,7 @@ class ResultViewModel2 : ViewModel() { val id: Int?, ) : LoadResponse - fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe { + fun loadSmall(searchResponse: SearchResponse) = ioSafe { val url = searchResponse.url _page.postValue(Resource.Loading(url)) _episodes.postValue(Resource.Loading()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index 5a23bfc1..8752e275 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -63,8 +63,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit, resView: AutofitRecyclerView diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 0a2ecb81..4ef5fa69 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -1,16 +1,11 @@ package com.lagradost.cloudstream3.ui.search import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.databinding.AccountSingleBinding import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding data class SearchHistoryItem( @@ -63,8 +58,7 @@ class SearchHistoryAdaptor( diffResult.dispatchUpdatesTo(this) } - class CardViewHolder - constructor( + class CardViewHolder( val binding: SearchHistoryItemBinding, private val clickCallback: (SearchHistoryCallback) -> Unit, ) : diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index f597132b..92575e58 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.search +import android.annotation.SuppressLint import android.content.Context import android.view.View import android.widget.ImageView @@ -37,16 +38,12 @@ object SearchResultBuilder { } } - /** - * @param nextFocusBehavior True if first, False if last, Null if between. - * Used to prevent escaping the adapter horizontally (focus wise). - */ + @SuppressLint("StringFormatInvalid") fun bind( clickCallback: (SearchClickCallback) -> Unit, card: SearchResponse, position: Int, itemView: View, - nextFocusBehavior: Boolean? = null, nextFocusUp: Int? = null, nextFocusDown: Int? = null, colorCallback : ((Palette) -> Unit)? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt index 9e03079f..71077e91 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt @@ -3,11 +3,9 @@ package com.lagradost.cloudstream3.ui.search import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +//TODO Relevance of this class since it's not used class SyncSearchViewModel { - private val repos = SyncApis - data class SyncSearchResultSearchResponse( override val name: String, override val url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt index 1dc79dc0..d7bd69f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.settings +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo) class AccountAdapter( - val cardList: List, + private val cardList: List, private val clickCallback: (AccountClickCallback) -> Unit ) : RecyclerView.Adapter() { @@ -42,12 +43,12 @@ class AccountAdapter( return cardList[position].accountIndex.toLong() } - class CardViewHolder - constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : + class CardViewHolder(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : RecyclerView.ViewHolder(binding.root) { // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! // private val accountName: TextView = itemView.findViewById(R.id.account_name)!! + @SuppressLint("StringFormatInvalid") fun bind(card: AuthAPI.LoginInfo) { // just in case name is null account index will show, should never happened binding.accountName.text = card.name ?: "%s %d".format( 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 6ba93c0f..88335eea 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 @@ -26,7 +26,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper 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 fd61962c..7cb1a848 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 @@ -28,10 +28,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 7560d75f..21707ca7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -10,7 +10,6 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn @@ -108,7 +107,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { getPref(R.string.hide_player_control_names_key)?.hideOn(TV) getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -116,7 +115,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_key), - Qualities.values().last().value + Qualities.entries.last().value ) activity?.showBottomDialog( @@ -132,7 +131,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { } getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -140,7 +139,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_mobile_data_key), - Qualities.values().last().value + Qualities.entries.last().value ) activity?.showBottomDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index cfb46c39..cb7d25fd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -34,7 +34,7 @@ class SettingsProviders : PreferenceFragmentCompat() { getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { activity?.getApiDubstatusSettings()?.let { current -> - val dublist = DubStatus.values() + val dublist = DubStatus.entries val names = dublist.map { it.name } val currentList = ArrayList() 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 4aaa5e12..260c6674 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 @@ -128,7 +128,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { - val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.getDefault()).format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { fileStream = VideoDownloadManager.setupStream( @@ -169,10 +169,10 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(currentInstaller), getString(R.string.apk_installer_settings), true, - {}) { + {}) { num -> try { settingsManager.edit() - .putInt(getString(R.string.apk_installer_key), prefValues[it]) + .putInt(getString(R.string.apk_installer_key), prefValues[num]) .apply() } catch (e: Exception) { logError(e) @@ -209,9 +209,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(current), getString(R.string.automatic_plugin_download_mode_title), true, - {}) { + {}) { num -> settingsManager.edit() - .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[num]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 909c30be..9fb3f282 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -1,9 +1,11 @@ package com.lagradost.cloudstream3.ui.settings.extensions +import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible @@ -27,11 +29,10 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import org.junit.Assert -import org.junit.Test import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 +import kotlin.math.pow data class PluginViewData( @@ -95,21 +96,13 @@ class PluginAdapter( } companion object { - private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max if (current >= target) return current return findClosestBase2(target, current * 2, max) } - @Test - fun testFindClosestBase2() { - Assert.assertEquals(16, findClosestBase2(0)) - Assert.assertEquals(256, findClosestBase2(170)) - Assert.assertEquals(256, findClosestBase2(256)) - Assert.assertEquals(512, findClosestBase2(257)) - Assert.assertEquals(512, findClosestBase2(700)) - } - private val iconSizeExact = 32.toPx private val iconSize by lazy { findClosestBase2(iconSizeExact, 16, 512) @@ -122,10 +115,7 @@ class PluginAdapter( val base = value / 3 return if (value >= 3 && base < suffix.size) { DecimalFormat("#0.00").format( - numValue / Math.pow( - 10.0, - (base * 3).toDouble() - ) + numValue / 10.0.pow((base * 3).toDouble()) ) + suffix[base] } else { DecimalFormat().format(numValue) @@ -136,6 +126,7 @@ class PluginAdapter( inner class PluginViewHolder(val binding: RepositoryItemBinding) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("SetTextI18n") fun bind( data: PluginViewData, ) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index c5319c37..4878049b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -190,7 +190,7 @@ class PluginsFragment : Fragment() { bindChips( binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), - TvType.values().toList(), + TvType.entries.toList(), callback = { list -> pluginViewModel.tvTypes.clear() pluginViewModel.tvTypes.addAll(list.map { it.name }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 56014eb4..fd5422b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -10,7 +10,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index f9197213..49a93608 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -10,7 +10,6 @@ import androidx.core.util.forEach import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index bb9558b8..c76a218e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -15,8 +15,11 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.media3.common.text.Cue import com.fasterxml.jackson.annotation.JsonProperty -import com.google.android.gms.cast.TextTrackStyle -import com.google.android.gms.cast.TextTrackStyle.* +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DROP_SHADOW +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_NONE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_RAISED import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent @@ -42,7 +45,7 @@ data class SaveChromeCaptionStyle( @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent @JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") var edgeType: Int = TextTrackStyle.EDGE_TYPE_OUTLINE, + @JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, @JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE, @JsonProperty("fontScale") var fontScale: Float = 1.05f, @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, @@ -99,7 +102,7 @@ class ChromecastSubtitlesFragment : Fragment() { } private fun onColorSelected(stuff: Pair) { - context?.setColor(stuff.first, stuff.second) + setColor(stuff.first, stuff.second) if (hide) activity?.hideSystemUI() } @@ -122,7 +125,7 @@ class ChromecastSubtitlesFragment : Fragment() { return if (color == Color.TRANSPARENT) Color.BLACK else color } - private fun Context.setColor(id: Int, color: Int?) { + private fun setColor(id: Int, color: Int?) { val realColor = color ?: getDefColor(id) when (id) { 0 -> state.foregroundColor = realColor @@ -135,7 +138,7 @@ class ChromecastSubtitlesFragment : Fragment() { updateState() } - private fun Context.updateState() { + private fun updateState() { //subtitle_text?.setStyle(fromSaveToStyle(state)) } @@ -173,7 +176,7 @@ class ChromecastSubtitlesFragment : Fragment() { fixPaddingStatusbar(binding?.subsRoot) state = getCurrentSavedStyle() - context?.updateState() + updateState() val isTvSettings = isLayout(TV or EMULATOR) @@ -195,7 +198,7 @@ class ChromecastSubtitlesFragment : Fragment() { } this.setOnLongClickListener { - it.context.setColor(id, null) + setColor(id, null) showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -247,13 +250,13 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.edgeType = edgeTypes.map { it.first }[index] - textView.context.updateState() + updateState() } } binding?.subsEdgeType?.setOnLongClickListener { state.edgeType = defaultState.edgeType - it.context.updateState() + updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -323,12 +326,12 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.fontFamily = fontTypes.map { it.first }[index] - textView.context.updateState() + updateState() } } - binding?.subsFont?.setOnLongClickListener { textView -> + binding?.subsFont?.setOnLongClickListener { _ -> state.fontFamily = defaultState.fontFamily - textView.context.updateState() + updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index 1466afed..8821905e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -14,11 +14,13 @@ import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.annotation.FontRes +import androidx.annotation.OptIn import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import androidx.media3.common.text.Cue +import androidx.media3.common.util.UnstableApi import androidx.media3.ui.CaptionStyleCompat import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -28,7 +30,6 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStore.setKey @@ -46,7 +47,7 @@ const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" -data class SaveCaptionStyle( +data class SaveCaptionStyle @OptIn(UnstableApi::class) constructor( @JsonProperty("foregroundColor") var foregroundColor: Int, @JsonProperty("backgroundColor") var backgroundColor: Int, @JsonProperty("windowColor") var windowColor: Int, @@ -67,7 +68,7 @@ data class SaveCaptionStyle( const val DEF_SUBS_ELEVATION = 20 -@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +@OptIn(androidx.media3.common.util.UnstableApi::class) class SubtitlesFragment : Fragment() { companion object { val applyStyleEvent = Event() @@ -167,7 +168,7 @@ class SubtitlesFragment : Fragment() { activity?.hideSystemUI() } - private fun onDialogDismissed(id: Int) { + private fun onDialogDismissed(@Suppress("UNUSED_PARAMETER") id: Int) { if (hide) activity?.hideSystemUI() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt index e9b69c5b..f0c948a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt @@ -83,7 +83,7 @@ object EpisodeSkip { startMs = start, endMs = end ) - }?.let { list -> + }.let { list -> out.addAll(list) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index f0aae7bc..b13de062 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -43,7 +43,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.tvprovider.media.tv.* import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor import androidx.viewpager2.widget.ViewPager2 -import com.fasterxml.jackson.module.kotlin.readValue import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.gms.common.ConnectionResult @@ -58,7 +57,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEv import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.RepositoryManager -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.player.SubtitleData @@ -161,7 +160,7 @@ object AppContextUtils { .setTitle(title) .setPosterArtUri(Uri.parse(card.posterUrl)) .setIntentUri(Uri.parse(card.id?.let { - "$appStringResumeWatching://$it" + "$APP_STRING_RESUME_WATCHING://$it" } ?: card.url)) .setInternalProviderId(card.url) .setLastEngagementTimeUtcMillis( 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 802c1a64..b25be59f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -81,12 +81,12 @@ object BackupUtils { // Kinda hack, but I couldn't think of a better way data class BackupVars( - @JsonProperty("_Bool") val _Bool: Map?, - @JsonProperty("_Int") val _Int: Map?, - @JsonProperty("_String") val _String: Map?, - @JsonProperty("_Float") val _Float: Map?, - @JsonProperty("_Long") val _Long: Map?, - @JsonProperty("_StringSet") val _StringSet: Map?>?, + @JsonProperty("_Bool") val bool: Map?, + @JsonProperty("_Int") val int: Map?, + @JsonProperty("_String") val string: Map?, + @JsonProperty("_Float") val float: Map?, + @JsonProperty("_Long") val long: Map?, + @JsonProperty("_StringSet") val stringSet: Map?>?, ) data class BackupFile( @@ -134,21 +134,21 @@ object BackupUtils { ) { if (context == null) return if (restoreSettings) { - context.restoreMap(backupFile.settings._Bool, true) - context.restoreMap(backupFile.settings._Int, true) - context.restoreMap(backupFile.settings._String, true) - context.restoreMap(backupFile.settings._Float, true) - context.restoreMap(backupFile.settings._Long, true) - context.restoreMap(backupFile.settings._StringSet, true) + context.restoreMap(backupFile.settings.bool, true) + context.restoreMap(backupFile.settings.int, true) + context.restoreMap(backupFile.settings.string, true) + context.restoreMap(backupFile.settings.float, true) + context.restoreMap(backupFile.settings.long, true) + context.restoreMap(backupFile.settings.stringSet, true) } if (restoreDataStore) { - context.restoreMap(backupFile.datastore._Bool) - context.restoreMap(backupFile.datastore._Int) - context.restoreMap(backupFile.datastore._String) - context.restoreMap(backupFile.datastore._Float) - context.restoreMap(backupFile.datastore._Long) - context.restoreMap(backupFile.datastore._StringSet) + context.restoreMap(backupFile.datastore.bool) + context.restoreMap(backupFile.datastore.int) + context.restoreMap(backupFile.datastore.string) + context.restoreMap(backupFile.datastore.float) + context.restoreMap(backupFile.datastore.long) + context.restoreMap(backupFile.datastore.stringSet) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 19c817b9..b5192aae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -56,16 +56,27 @@ data class Editor( ) { /** Always remember to call apply after */ fun setKeyRaw(path: String, value: T) { - when (value) { - is Boolean -> editor.putBoolean(path, value) - is Int -> editor.putInt(path, value) - is String -> editor.putString(path, value) - is Float -> editor.putFloat(path, value) - is Long -> editor.putLong(path, value) - (value as? Set != null) -> editor.putStringSet(path, value as Set) + @Suppress("UNCHECKED_CAST") + if (isStringSet(value)) { + editor.putStringSet(path, value as Set) + } else { + when (value) { + is Boolean -> editor.putBoolean(path, value) + is Int -> editor.putInt(path, value) + is String -> editor.putString(path, value) + is Float -> editor.putFloat(path, value) + is Long -> editor.putLong(path, value) + } } } + private fun isStringSet(value: Any?) : Boolean { + if (value is Set<*>) { + return value.filterIsInstance().size == value.size + } + return false + } + fun apply() { editor.apply() System.gc() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index 421e4420..c92da214 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -7,7 +7,6 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index 89bb0031..59f534ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -32,26 +32,26 @@ import java.io.InputStreamReader class InAppUpdater { companion object { - const val GITHUB_USER_NAME = "recloudstream" - const val GITHUB_REPO = "cloudstream" + private const val GITHUB_USER_NAME = "recloudstream" + private const val GITHUB_REPO = "cloudstream" - const val LOG_TAG = "InAppUpdater" + private const val LOG_TAG = "InAppUpdater" // === IN APP UPDATER === data class GithubAsset( @JsonProperty("name") val name: String, @JsonProperty("size") val size: Int, // Size bytes - @JsonProperty("browser_download_url") val browser_download_url: String, // download link - @JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive + @JsonProperty("browser_download_url") val browserDownloadUrl: String, // download link + @JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive ) data class GithubRelease( - @JsonProperty("tag_name") val tag_name: String, // Version code + @JsonProperty("tag_name") val tagName: String, // Version code @JsonProperty("body") val body: String, // Desc @JsonProperty("assets") val assets: List, - @JsonProperty("target_commitish") val target_commitish: String, // branch + @JsonProperty("target_commitish") val targetCommitish: String, // branch @JsonProperty("prerelease") val prerelease: Boolean, - @JsonProperty("node_id") val node_id: String //Node Id + @JsonProperty("node_id") val nodeId: String //Node Id ) data class GithubObject( @@ -61,7 +61,7 @@ class InAppUpdater { ) data class GithubTag( - @JsonProperty("object") val github_object: GithubObject, + @JsonProperty("object") val githubObject: GithubObject, ) data class Update( @@ -114,7 +114,7 @@ class InAppUpdater { response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> + release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 )?.groupValues?.let { @@ -134,7 +134,7 @@ class InAppUpdater { foundAsset?.name?.let { assetName -> val foundVersion = versionRegex.find(assetName) val shouldUpdate = - if (foundAsset.browser_download_url != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> + if (foundAsset.browserDownloadUrl != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> versionRegexLocal.find(versionName)?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } @@ -146,10 +146,10 @@ class InAppUpdater { return if (foundVersion != null) { Update( shouldUpdate, - foundAsset.browser_download_url, + foundAsset.browserDownloadUrl, foundVersion.groupValues[2], found.body, - found.node_id + found.nodeId ) } else { Update(false, null, null, null, null) @@ -168,33 +168,33 @@ class InAppUpdater { val found = response.lastOrNull { rel -> - rel.prerelease || rel.tag_name == "pre-release" + rel.prerelease || rel.tagName == "pre-release" } val foundAsset = found?.assets?.filter { it -> - it.content_type == "application/vnd.android.package-archive" + it.contentType == "application/vnd.android.package-archive" }?.getOrNull(0) val tagResponse = parseJson(app.get(tagUrl, headers = headers).text) - Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.github_object.sha.take(7)}") + Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.githubObject.sha.take(7)}") val shouldUpdate = (getString(R.string.commit_hash) .trim { c -> c.isWhitespace() } .take(7) != - tagResponse.github_object.sha + tagResponse.githubObject.sha .trim { c -> c.isWhitespace() } .take(7)) return if (foundAsset != null) { Update( shouldUpdate, - foundAsset.browser_download_url, - tagResponse.github_object.sha.take(10), + foundAsset.browserDownloadUrl, + tagResponse.githubObject.sha.take(10), found.body, - found.node_id + found.nodeId ) } else { Update(false, null, null, null, null) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt index bc81a5b9..4b3f02f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt @@ -11,7 +11,6 @@ import android.os.Build import android.widget.Toast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.Coroutines.main import java.io.InputStream @@ -57,7 +56,7 @@ class ApkInstaller(private val service: PackageInstallerService) { PackageInstaller.STATUS_FAILURE )) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT) + val userAction = intent.getSafeParcelableExtra(Intent.EXTRA_INTENT) userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(userAction) } @@ -146,3 +145,5 @@ class ApkInstaller(private val service: PackageInstallerService) { } } +@Suppress("DEPRECATION") +inline fun Intent.getSafeParcelableExtra(key: String): T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelableExtra(key) else getParcelableExtra(key, T::class.java) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt index 27609730..0d3da8e7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -17,8 +17,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -const val packageName = BuildConfig.APPLICATION_ID -const val TAG = "PowerManagerAPI" +private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID +private const val TAG = "PowerManagerAPI" object BatteryOptimizationChecker { @@ -72,7 +72,7 @@ object BatteryOptimizationChecker { val intent = Intent() try { intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.fromParts("package", packageName, null)) + .setData(Uri.fromParts("package", PACKAGE_NAME, null)) context.startActivity(intent, Bundle()) } catch (t: Throwable) { Log.e(TAG, "Unable to invoke any intent", t) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 71d3a1ef..351e77c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -73,8 +73,8 @@ object SyncUtil { val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text val mapped = parseJson(response) - val overrideMal = mapped?.malId ?: mapped?.Mal?.id ?: mapped?.Anilist?.malId - val overrideAnilist = mapped?.aniId ?: mapped?.Anilist?.id + val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId + val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id if (overrideMal != null) { return overrideMal.toString() to overrideAnilist?.toString() @@ -135,8 +135,8 @@ object SyncUtil { @JsonProperty("createdAt") val createdAt: String?, @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("deletedAt") val deletedAt: String?, - @JsonProperty("Mal") val Mal: Mal?, - @JsonProperty("Anilist") val Anilist: Anilist?, + @JsonProperty("Mal") val mal: Mal?, + @JsonProperty("Anilist") val anilist: Anilist?, @JsonProperty("malUrl") val malUrl: String? ) 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 8670de53..ad1b6502 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -553,7 +553,7 @@ object UIHelper { return result } - fun Context?.IsBottomLayout(): Boolean { + fun Context?.isBottomLayout(): Boolean { if (this == null) return true val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) return settingsManager.getBoolean(getString(R.string.bottom_title_key), true) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 197bacc6..a3f6d789 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -293,6 +293,7 @@ object VideoDownloadManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } else { + //fixme Specify a better flag PendingIntent.getActivity(context, 0, intent, 0) } builder.setContentIntent(pendingIntent) @@ -475,10 +476,10 @@ object VideoDownloadManager { } } - private const val reservedChars = "|\\?*<\":>+[]/\'" + private const val RESERVED_CHARS = "|\\?*<\":>+[]/\'" fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String { var tempName = name - for (c in reservedChars) { + for (c in RESERVED_CHARS) { tempName = tempName.replace(c, ' ') } if (removeSpaces) tempName = tempName.replace(" ", "") @@ -1699,7 +1700,7 @@ object VideoDownloadManager { } */ fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = - getDownloadFileInfo(context, id, removeKeys = true) + getDownloadFileInfo(context, id) private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) @@ -1709,7 +1710,6 @@ object VideoDownloadManager { private fun getDownloadFileInfo( context: Context, id: Int, - removeKeys: Boolean = false ): DownloadedFileInfoResult? { try { val info = diff --git a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt index d4725d53..2aea0b8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt @@ -19,7 +19,7 @@ class FlowLayout : ViewGroup { @SuppressLint("CustomViewStyleable") internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { val t = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout) - itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0); + itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0) t.recycle() } diff --git a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt new file mode 100644 index 00000000..5dbf4d7c --- /dev/null +++ b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2 +import org.junit.Assert +import org.junit.Test + +class PluginAdapterTest { + @Test + fun testFindClosestBase2() { + Assert.assertEquals(16, findClosestBase2(0)) + Assert.assertEquals(256, findClosestBase2(170)) + Assert.assertEquals(256, findClosestBase2(256)) + Assert.assertEquals(512, findClosestBase2(257)) + Assert.assertEquals(512, findClosestBase2(700)) + } +} \ No newline at end of file From 82f8ab489e88145e0f498dc3537d26d59157b7dc Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:58:35 +0200 Subject: [PATCH 134/157] Fix prerelease test function --- .../ui/settings/extensions/PluginAdapter.kt | 17 ++++++++++++++--- .../lagradost/cloudstream3/PluginAdapterTest.kt | 16 ---------------- 2 files changed, 14 insertions(+), 19 deletions(-) delete mode 100644 app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 9fb3f282..d159539d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -33,7 +33,8 @@ import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 import kotlin.math.pow - +import org.junit.Test +import org.junit.Assert data class PluginViewData( val plugin: Plugin, @@ -96,13 +97,23 @@ class PluginAdapter( } companion object { - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { + private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max if (current >= target) return current return findClosestBase2(target, current * 2, max) } + // DO NOT MOVE, as running this test will result in ExceptionInInitializerError on prerelease due to static variables using Resources.getSystem() + // this test function is only to show how the function works + @Test + fun testFindClosestBase2() { + Assert.assertEquals(16, findClosestBase2(0)) + Assert.assertEquals(256, findClosestBase2(170)) + Assert.assertEquals(256, findClosestBase2(256)) + Assert.assertEquals(512, findClosestBase2(257)) + Assert.assertEquals(512, findClosestBase2(700)) + } + private val iconSizeExact = 32.toPx private val iconSize by lazy { findClosestBase2(iconSizeExact, 16, 512) diff --git a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt deleted file mode 100644 index 5dbf4d7c..00000000 --- a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.lagradost.cloudstream3 - -import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2 -import org.junit.Assert -import org.junit.Test - -class PluginAdapterTest { - @Test - fun testFindClosestBase2() { - Assert.assertEquals(16, findClosestBase2(0)) - Assert.assertEquals(256, findClosestBase2(170)) - Assert.assertEquals(256, findClosestBase2(256)) - Assert.assertEquals(512, findClosestBase2(257)) - Assert.assertEquals(512, findClosestBase2(700)) - } -} \ No newline at end of file From 150ad5fc9f4c90a8de8c42bd53b190a594606156 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:00:44 +0200 Subject: [PATCH 135/157] Add sorting by release date (#1206) --- .../cloudstream3/syncproviders/SyncApi.kt | 6 +++++- .../syncproviders/providers/AniListApi.kt | 10 +++++++--- .../syncproviders/providers/LocalList.kt | 5 +++++ .../syncproviders/providers/MALApi.kt | 9 +++++++++ .../syncproviders/providers/SimklApi.kt | 7 ++++++- .../ui/library/LibraryViewModel.kt | 4 +++- .../cloudstream3/utils/DataStoreHelper.kt | 20 +++++++++++++++---- app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 53 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt index 878e0cb3..dcb8bbea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiText import me.xdrop.fuzzywuzzy.FuzzySearch +import java.util.Date interface SyncAPI : OAuth2API { /** @@ -124,6 +125,8 @@ interface SyncAPI : OAuth2API { ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed() ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) } ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime } + ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate } + ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate } else -> items } } @@ -158,9 +161,10 @@ interface SyncAPI : OAuth2API { override var posterUrl: String?, override var posterHeaders: Map?, override var quality: SearchQuality?, + val releaseDate: Date?, override var id: Int? = null, val plot : String? = null, val rating: Int? = null, - val tags: List? = null, + val tags: List? = null ) : SearchResponse } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index e51d3d65..6112c7db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -16,15 +16,16 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject +import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import java.net.URL import java.net.URLEncoder -import java.util.* +import java.util.Locale class AniListApi(index: Int) : AccountManager(index), SyncAPI { override var name = "AniList" @@ -631,8 +632,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ?: this.media.coverImage.medium, null, null, + this.media.seasonYear.toYear(), null, - plot = this.media.description + plot = this.media.description, ) } } @@ -689,6 +691,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) 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 f819cd3b..0d9a4d13 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -119,6 +119,11 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, +// ListSorting.RatingHigh, +// ListSorting.RatingLow, + ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 6046a0f2..08c18653 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -27,6 +27,7 @@ import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat import java.time.Instant +import java.time.format.DateTimeFormatter import java.util.Calendar import java.util.Date import java.util.Locale @@ -448,6 +449,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { null, null, plot = this.node.synopsis, + releaseDate = if (this.node.startDate == null) null else try {Date.from( + Instant.from( + DateTimeFormatter.ofPattern(if (this.node.startDate.length == 4) "yyyy" else if (this.node.startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") + .parse(this.node.startDate) + ) + )} catch (_: RuntimeException) {null} ) } } @@ -512,6 +519,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index e5db626b..50517f9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import okhttp3.Interceptor import okhttp3.Response import java.math.BigInteger @@ -670,7 +671,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.poster?.let { getPosterUrl(it) }, null, null, - movie.ids.simkl, + this.movie.year?.toYear(), + movie.ids.simkl ) } } @@ -702,6 +704,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.poster?.let { getPosterUrl(it) }, null, null, + this.show.year?.toYear(), show.ids.simkl ) } @@ -1027,6 +1030,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 1bd01c86..6c602e6c 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 @@ -23,6 +23,8 @@ enum class ListSorting(@StringRes val stringRes: Int) { UpdatedOld(R.string.sort_updated_old), AlphabeticalA(R.string.sort_alphabetical_a), AlphabeticalZ(R.string.sort_alphabetical_z), + ReleaseDateNew(R.string.sort_release_date_new), + ReleaseDateOld(R.string.sort_release_date_old), } const val LAST_SYNC_API_KEY = "last_sync_api" @@ -132,4 +134,4 @@ class LibraryViewModel : ViewModel() { MainActivity.reloadLibraryEvent -= ::reloadPages super.onCleared() } -} \ No newline at end of file +} 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 43124a53..2fa5f6a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -2,8 +2,8 @@ package com.lagradost.cloudstream3.utils import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys @@ -11,6 +11,13 @@ 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.CommonActivity.showToast +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.EpisodeResponse +import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchQuality +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType @@ -18,6 +25,9 @@ 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.utils.AppContextUtils.filterProviderByPreferredMedia +import java.util.Calendar +import java.util.Date +import java.util.GregorianCalendar import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -195,6 +205,8 @@ object DataStoreHelper { return this } + fun Int.toYear() : Date = GregorianCalendar.getInstance().also { it.set(Calendar.YEAR, this) }.time + /** * Used to display notifications on new episodes and posters in library. **/ @@ -242,7 +254,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -273,7 +285,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -304,7 +316,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21067fff..37a3f993 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,6 +797,8 @@ Can\'t get the device PIN code, try local authentication PIN code is now expired ! Code expires in %1$dm %2$ds + Release Date (New to Old) + Release Date (Old to New) hide_player_control_names_key Hide names of the player\'s controls \ No newline at end of file From b2f08847e1dc736aa54a26c84d8f71e8ea83c166 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:01:45 +0200 Subject: [PATCH 136/157] Add system dark theme (#1208) --- app/src/main/AndroidManifest.xml | 2 +- .../lagradost/cloudstream3/CommonActivity.kt | 28 +++++++++++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 2 ++ .../cloudstream3/ui/settings/SettingsUI.kt | 14 +++++++--- app/src/main/res/values-es/array.xml | 2 ++ app/src/main/res/values-pl/array.xml | 2 ++ app/src/main/res/values-tr/array.xml | 2 ++ app/src/main/res/values-vi/array.xml | 2 ++ app/src/main/res/values/array.xml | 2 ++ 9 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a23ef725..888be999 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,7 +97,7 @@ --> = Build.VERSION_CODES.Q) { + loadThemes(act) + } + } + + private fun mapSystemTheme(act: Activity): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val currentNightMode = + act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return when (currentNightMode) { + Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme + else -> R.style.AppTheme // Night mode is active, we're using dark theme + } + } else { + return R.style.AppTheme + } + } + fun loadThemes(act: Activity?) { if (act == null) return val settingsManager = PreferenceManager.getDefaultSharedPreferences(act) val currentTheme = when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) { + "System" -> mapSystemTheme(act) "Black" -> R.style.AppTheme "Light" -> R.style.LightMode "Amoled" -> R.style.AmoledMode @@ -352,8 +376,8 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ - private fun View.hasContent() : Boolean { - return isShown && when(this) { + private fun View.hasContent(): Boolean { + return isShown && when (this) { //is RecyclerView -> this.childCount > 0 is ViewGroup -> this.childCount > 0 else -> true diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index eed69a50..b59265ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -68,6 +68,7 @@ import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.setActivityInstance import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale +import com.lagradost.cloudstream3.CommonActivity.updateTheme import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding @@ -484,6 +485,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateLocale() // android fucks me by chaining lang when rotating the phone + updateTheme(this) // Update if system theme val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index cc14e761..8c3ad0ad 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -88,10 +88,9 @@ class SettingsUI : PreferenceFragmentCompat() { getPref(R.string.app_theme_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_names).toMutableList() val prefValues = resources.getStringArray(R.array.themes_names_values).toMutableList() - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + val removeIncompatible = { text: String -> val toRemove = prefValues - .mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null } + .mapIndexed { idx, s -> if (s.startsWith(text)) idx else null } .filterNotNull() var offset = 0 toRemove.forEach { idx -> @@ -100,6 +99,12 @@ class SettingsUI : PreferenceFragmentCompat() { offset += 1 } } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + removeIncompatible("Monet") + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // Remove system on android 9 and less + removeIncompatible("System") + } val currentLayout = settingsManager.getString(getString(R.string.app_theme_key), prefValues.first()) @@ -123,7 +128,8 @@ class SettingsUI : PreferenceFragmentCompat() { } getPref(R.string.primary_color_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_overlay_names).toMutableList() - val prefValues = resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() + val prefValues = + resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less val toRemove = prefValues diff --git a/app/src/main/res/values-es/array.xml b/app/src/main/res/values-es/array.xml index 05d49f98..eb197f43 100644 --- a/app/src/main/res/values-es/array.xml +++ b/app/src/main/res/values-es/array.xml @@ -247,6 +247,7 @@ Gris Amoled Destello + Sistema Material You @@ -254,6 +255,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml index 9f76f423..a43d7bcf 100644 --- a/app/src/main/res/values-pl/array.xml +++ b/app/src/main/res/values-pl/array.xml @@ -256,6 +256,7 @@ Szary Amoled Flashbang + System Material You @@ -263,6 +264,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml index 5c723f72..22a94ebf 100644 --- a/app/src/main/res/values-tr/array.xml +++ b/app/src/main/res/values-tr/array.xml @@ -281,6 +281,7 @@ Gri Amoled Flaş Bombası + Sistem Material You @@ -288,6 +289,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml index aac94100..f363befd 100644 --- a/app/src/main/res/values-vi/array.xml +++ b/app/src/main/res/values-vi/array.xml @@ -248,6 +248,7 @@ Xám Amoled Sáng + Hệ thống Material You @@ -255,6 +256,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 3be12510..03715faf 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -318,6 +318,7 @@ Gray Amoled Flashbang + System Material You @@ -325,6 +326,7 @@ Black Amoled Light + System Monet From 63e27c2ea5c7c57d41846038678c38654e28e495 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 30 Jul 2024 21:16:11 +0300 Subject: [PATCH 137/157] Fix Trailers on API<33 (#1226) Recent NewPipeExtractor updates pushed minimum sdk to 33 which needs desugar_jdk_libs_nio --- app/build.gradle.kts | 8 ++++---- library/build.gradle.kts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2040cf39..ee6cda6c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -140,7 +140,7 @@ android { abortOnError = false checkReleaseBuilds = false } - + buildFeatures { buildConfig = true } @@ -200,7 +200,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:2d36945") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding @@ -213,7 +213,7 @@ dependencies { implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors implementation("androidx.tvprovider:tvprovider:1.0.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures - implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication + implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV @@ -223,7 +223,7 @@ dependencies { implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API Level 25 or Less. */ diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 516e1ee9..00bc3c14 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -27,7 +27,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") implementation("me.xdrop:fuzzywuzzy:1.4.0") // Match extractors implementation("org.mozilla:rhino:1.7.15") // run JavaScript - implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") + implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") } } } From 30adb1cd9d8aa7d538d027fb4e9506cd31224869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Tue, 30 Jul 2024 21:38:51 +0300 Subject: [PATCH 138/157] fixed: Test Search & VidMoxy, RapidVid extractors (#1219) --- .../cloudstream3/utils/TestingUtils.kt | 5 ++- .../extractors/ContentXExtractor.kt | 41 +++++++++---------- .../extractors/HDMomPlayerExtractor.kt | 21 +++++----- .../extractors/HDPlayerSystemExtractor.kt | 23 +++++------ .../extractors/MailRuExtractor.kt | 21 ++++------ .../extractors/OdnoklassnikiExtractor.kt | 18 ++++---- .../extractors/PeaceMakerstExtractor.kt | 37 ++++++++--------- .../extractors/RapidVidExtractor.kt | 37 ++++++++++------- .../extractors/SibNetExtractor.kt | 11 +++-- .../cloudstream3/extractors/TRsTXExtractor.kt | 30 +++++++------- .../extractors/TauVideoExtractor.kt | 11 +++-- .../extractors/VidMoxyExtractor.kt | 37 ++++++++++------- .../extractors/VideoSeyredExtractor.kt | 13 +++--- 13 files changed, 155 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 5e2b2bc1..049f92fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -4,6 +4,7 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import org.junit.Assert +import kotlin.random.Random object TestingUtils { open class TestResult(val success: Boolean) { @@ -280,8 +281,8 @@ object TestingUtils { // Test Search Results val searchQueries = - // Use the first 3 home page results as queries since they are guaranteed to exist - (homePageList.take(3).map { it.name } + + // Use the random 3 home page results as queries since they are guaranteed to exist + (homePageList.shuffled(Random).take(3).map { it.name.split(" ").first() } + // If home page is sparse then use generic search queries listOf("over", "iron", "guy")).take(3) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt index 27a5c52a..13a717b6 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt @@ -12,53 +12,52 @@ open class ContentX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text - val i_extract = Regex("""window\.openPlayer\('([^']+)'""").find(i_source)!!.groups[1]?.value ?: throw ErrorLoadingException("i_extract is null") + val iSource = app.get(url, referer=extRef).text + val iExtract = Regex("""window\.openPlayer\('([^']+)'""").find(iSource)!!.groups[1]?.value ?: throw ErrorLoadingException("iExtract is null") - val sub_urls = mutableSetOf() - Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(i_source).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(iSource).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val vid_source = app.get("${mainUrl}/source2.php?v=${i_extract}", referer=ext_ref).text - val vid_extract = Regex("""file\":\"([^\"]+)""").find(vid_source)!!.groups[1]?.value ?: throw ErrorLoadingException("vid_extract is null") - val m3u_link = vid_extract.replace("\\", "") + val vidSource = app.get("${mainUrl}/source2.php?v=${iExtract}", referer=extRef).text + val vidExtract = Regex("""file\":\"([^\"]+)""").find(vidSource)!!.groups[1]?.value ?: throw ErrorLoadingException("vidExtract is null") + val m3uLink = vidExtract.replace("\\", "") callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, + url = m3uLink, referer = url, quality = Qualities.Unknown.value, isM3u8 = true ) ) - val i_dublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(i_source)!!.groups[1]?.value - if (i_dublaj != null) { - val dublaj_source = app.get("${mainUrl}/source2.php?v=${i_dublaj}", referer=ext_ref).text - val dublaj_extract = Regex("""file\":\"([^\"]+)""").find(dublaj_source)!!.groups[1]?.value ?: throw ErrorLoadingException("dublaj_extract is null") - val dublaj_link = dublaj_extract.replace("\\", "") + val iDublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(iSource)!!.groups[1]?.value + if (iDublaj != null) { + val dublajSource = app.get("${mainUrl}/source2.php?v=${iDublaj}", referer=extRef).text + val dublajExtract = Regex("""file\":\"([^\"]+)""").find(dublajSource)!!.groups[1]?.value ?: throw ErrorLoadingException("dublajExtract is null") + val dublajLink = dublajExtract.replace("\\", "") callback.invoke( ExtractorLink( source = "${this.name} Türkçe Dublaj", name = "${this.name} Türkçe Dublaj", - url = dublaj_link, + url = dublajLink, referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 1f70ce61..1152cb4b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -16,24 +16,23 @@ open class HDMomPlayer : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3u_link:String? - val ext_ref = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text + val m3uLink:String? + val extRef = referer ?: "" + val iSource = app.get(url, referer=extRef).text - val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(i_source)?.groupValues + val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(iSource)?.groupValues if (bePlayer != null) { val bePlayerPass = bePlayer.get(1) val bePlayerData = bePlayer.get(2) val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - Log.d("Kekik_${this.name}", "encrypted » ${encrypted}") - m3u_link = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) + m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) } else { - m3u_link = Regex("""file:\"([^\"]+)""").find(i_source)?.groupValues?.get(1) + m3uLink = Regex("""file:\"([^\"]+)""").find(iSource)?.groupValues?.get(1) - val track_str = Regex("""tracks:\[([^\]]+)""").find(i_source)?.groupValues?.get(1) - if (track_str != null) { - val tracks:List = jacksonObjectMapper().readValue("[${track_str}]") + val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1) + if (trackStr != null) { + val tracks:List = jacksonObjectMapper().readValue("[${trackStr}]") for (track in tracks) { if (track.file == null || track.label == null) continue @@ -53,7 +52,7 @@ open class HDMomPlayer : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), + url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index 8318c3fb..e3cf3aee 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -13,37 +13,36 @@ open class HDPlayerSystem : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val vid_id = if (url.contains("video/")) { + val extRef = referer ?: "" + val vidId = if (url.contains("video/")) { url.substringAfter("video/") } else { url.substringAfter("?data=") } - val post_url = "${mainUrl}/player/index.php?data=${vid_id}&do=getVideo" - Log.d("Kekik_${this.name}", "post_url » ${post_url}") + val postUrl = "${mainUrl}/player/index.php?data=${vidId}&do=getVideo" val response = app.post( - post_url, + postUrl, data = mapOf( - "hash" to vid_id, - "r" to ext_ref + "hash" to vidId, + "r" to extRef ), - referer = ext_ref, + referer = extRef, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) - val video_response = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") - val m3u_link = video_response.securedLink + val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") + val m3uLink = videoResponse.securedLink callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, - referer = ext_ref, + url = m3uLink, + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index ce742e97..07346c70 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -13,28 +13,25 @@ open class MailRu : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val vid_id = url.substringAfter("video/embed/").trim() - val video_req = app.get("${mainUrl}/+/video/meta/${vid_id}", referer=url) - val video_key = video_req.cookies["video_key"].toString() - Log.d("Kekik_${this.name}", "video_key » ${video_key}") + val vidId = url.substringAfter("video/embed/").trim() + val videoReq = app.get("${mainUrl}/+/video/meta/${vidId}", referer=url) + val videoKey = videoReq.cookies["video_key"].toString() - val video_data = AppUtils.tryParseJson(video_req.text) ?: throw ErrorLoadingException("Video not found") + val videoData = AppUtils.tryParseJson(videoReq.text) ?: throw ErrorLoadingException("Video not found") - for (video in video_data.videos) { - Log.d("Kekik_${this.name}", "video » ${video}") + for (video in videoData.videos) { - val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = video_url, + url = videoUrl, referer = url, - headers = mapOf("Cookie" to "video_key=${video_key}"), + headers = mapOf("Cookie" to "video_key=${videoKey}"), quality = getQualityFromName(video.key), isM3u8 = false ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 6db0830c..31b3d50b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -13,22 +13,20 @@ open class Odnoklassniki : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val user_agent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36") + val userAgent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36") - val video_req = app.get(url, headers=user_agent).text.replace("\\"", "\"").replace("\\\\", "\\") + val videoReq = app.get(url, headers=userAgent).text.replace("\\"", "\"").replace("\\\\", "\\") .replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult -> Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString() } - val videos_str = Regex("""\"videos\":(\[[^\]]*\])""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") - val videos = AppUtils.tryParseJson>(videos_str) ?: throw ErrorLoadingException("Video not found") + val videosStr = Regex("""\"videos\":(\[[^\]]*\])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") + val videos = AppUtils.tryParseJson>(videosStr) ?: throw ErrorLoadingException("Video not found") for (video in videos) { - Log.d("Kekik_${this.name}", "video » ${video}") - val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url val quality = video.name.uppercase() .replace("MOBILE", "144p") @@ -44,10 +42,10 @@ open class Odnoklassniki : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = video_url, + url = videoUrl, referer = url, quality = getQualityFromName(quality), - headers = user_agent, + headers = userAgent, isM3u8 = false ) ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index 0a005036..3a5cf727 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -13,39 +13,38 @@ open class PeaceMakerst : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3u_link:String? - val ext_ref = referer ?: "" - val post_url = "${url}?do=getVideo" - Log.d("Kekik_${this.name}", "post_url » ${post_url}") + val m3uLink:String? + val extRef = referer ?: "" + val postUrl = "${url}?do=getVideo" val response = app.post( - post_url, + postUrl, data = mapOf( "hash" to url.substringAfter("video/"), - "r" to ext_ref, + "r" to extRef, "s" to "" ), - referer = ext_ref, + referer = extRef, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) if (response.text.contains("teve2.com.tr\\/embed\\/")) { - val teve2_id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") - val teve2_response = app.get( - "https://www.teve2.com.tr/action/media/${teve2_id}", - referer = "https://www.teve2.com.tr/embed/${teve2_id}" + val teve2Id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") + val teve2Response = app.get( + "https://www.teve2.com.tr/action/media/${teve2Id}", + referer = "https://www.teve2.com.tr/embed/${teve2Id}" ).parsedSafe() ?: throw ErrorLoadingException("teve2 response is null") - m3u_link = teve2_response.media.link.serviceUrl + "//" + teve2_response.media.link.securePath + m3uLink = teve2Response.media.link.serviceUrl + "//" + teve2Response.media.link.securePath } else { - val video_response = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") - val video_sources = video_response.videoSources - if (video_sources.isNotEmpty()) { - m3u_link = video_sources.lastOrNull()?.file + val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") + val videoSources = videoResponse.videoSources + if (videoSources.isNotEmpty()) { + m3uLink = videoSources.lastOrNull()?.file } else { - m3u_link = null + m3uLink = null } } @@ -53,8 +52,8 @@ open class PeaceMakerst : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), - referer = ext_ref, + url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index 607d2d78..1088f2e9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -12,36 +12,45 @@ open class RapidVid : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val extRef = referer ?: "" + val videoReq = app.get(url, referer=extRef).text - val sub_urls = mutableSetOf() - Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(videoReq).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + var extractedValue = Regex("""file": "(.*)",""").find(videoReq)?.groupValues?.get(1) + var decoded: String? = null - val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - val decoded = String(bytes, Charsets.UTF_8) - Log.d("Kekik_${this.name}", "decoded » ${decoded}") + if (extractedValue != null) { + val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + decoded = String(bytes, Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } else { + val evalJWSsetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val JWSsetup = getAndUnpack(getAndUnpack(evalJWSsetup)).replace("\\\\", "\\") + extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "") + + val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } callback.invoke( ExtractorLink( source = this.name, name = this.name, url = decoded, - referer = ext_ref, + referer = extRef, quality = Qualities.Unknown.value, isM3u8 = true ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt index ebd57f9c..89f731f7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt @@ -12,18 +12,17 @@ open class SibNet : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text - var m3u_link = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(i_source)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") + val extRef = referer ?: "" + val iSource = app.get(url, referer=extRef).text + var m3uLink = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(iSource)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") - m3u_link = "${mainUrl}${m3u_link}" - Log.d("Kekik_${this.name}", "m3u_link » ${m3u_link}") + m3uLink = "${mainUrl}${m3uLink}" callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, + url = m3uLink, referer = url, quality = Qualities.Unknown.value, type = INFER_TYPE diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index de5ca9a2..f2a75b94 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -13,13 +13,13 @@ open class TRsTX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" + val extRef = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val videoReq = app.get(url, referer=extRef).text - val file = Regex("""file\":\"([^\"]+)""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val postLink = "${mainUrl}/" + file.replace("\\", "") - val rawList = app.post(postLink, referer=ext_ref).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") + val rawList = app.post(postLink, referer=extRef).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") val postJson: List = rawList.drop(1).map { item -> val mapItem = item as Map<*, *> @@ -28,37 +28,35 @@ open class TRsTX : ExtractorApi() { file = mapItem["file"] as? String ) } - Log.d("Kekik_${this.name}", "postJson » ${postJson}") - val vid_links = mutableSetOf() - val vid_map = mutableListOf>() + val vidLinks = mutableSetOf() + val vidMap = mutableListOf>() for (item in postJson) { if (item.file == null || item.title == null) continue val fileUrl = "${mainUrl}/playlist/" + item.file.substring(1) + ".txt" - val videoData = app.post(fileUrl, referer=ext_ref).text + val videoData = app.post(fileUrl, referer=extRef).text - if (videoData in vid_links) { continue } - vid_links.add(videoData) + if (videoData in vidLinks) { continue } + vidLinks.add(videoData) - vid_map.add(mapOf( + vidMap.add(mapOf( "title" to item.title, "videoData" to videoData )) } - for (mapEntry in vid_map) { - Log.d("Kekik_${this.name}", "mapEntry » ${mapEntry}") + for (mapEntry in vidMap) { val title = mapEntry["title"] ?: continue - val m3u_link = mapEntry["videoData"] ?: continue + val m3uLink = mapEntry["videoData"] ?: continue callback.invoke( ExtractorLink( source = this.name, name = "${this.name} - ${title}", - url = m3u_link, - referer = ext_ref, + url = m3uLink, + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 157374a3..0893b4de 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -13,12 +13,11 @@ open class TauVideo : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_key = url.split("/").last() - val video_url = "${mainUrl}/api/video/${video_key}" - Log.d("Kekik_${this.name}", "video_url » ${video_url}") + val extRef = referer ?: "" + val videoKey = url.split("/").last() + val videoUrl = "${mainUrl}/api/video/${videoKey}" - val api = app.get(video_url).parsedSafe() ?: throw ErrorLoadingException("TauVideo") + val api = app.get(videoUrl).parsedSafe() ?: throw ErrorLoadingException("TauVideo") for (video in api.urls) { callback.invoke( @@ -26,7 +25,7 @@ open class TauVideo : ExtractorApi() { source = this.name, name = this.name, url = video.url, - referer = ext_ref, + referer = extRef, quality = getQualityFromName(video.label), type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index e57772ce..f7c3dd5e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -12,36 +12,45 @@ open class VidMoxy : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val extRef = referer ?: "" + val videoReq = app.get(url, referer=extRef).text - val sub_urls = mutableSetOf() - Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(videoReq).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + var extractedValue = Regex("""file": "(.*)",""").find(videoReq)?.groupValues?.get(1) + var decoded: String? = null - val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - val decoded = String(bytes, Charsets.UTF_8) - Log.d("Kekik_${this.name}", "decoded » ${decoded}") + if (extractedValue != null) { + val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + decoded = String(bytes, Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } else { + val evaljwSetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val jwSetup = getAndUnpack(getAndUnpack(evaljwSetup)).replace("\\\\", "\\") + extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "") + + val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } callback.invoke( ExtractorLink( source = this.name, name = this.name, url = decoded, - referer = ext_ref, + referer = extRef, quality = Qualities.Unknown.value, isM3u8 = true ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index 1161ff66..c85e6416 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -15,14 +15,13 @@ open class VideoSeyred : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_id = url.substringAfter("embed/").substringBefore("?") - val video_url = "${mainUrl}/playlist/${video_id}.json" - Log.d("Kekik_${this.name}", "video_url » ${video_url}") + val extRef = referer ?: "" + val videoId = url.substringAfter("embed/").substringBefore("?") + val videoUrl = "${mainUrl}/playlist/${videoId}.json" - val response_raw = app.get(video_url) - val response_list:List = jacksonObjectMapper().readValue(response_raw.text) ?: throw ErrorLoadingException("VideoSeyred") - val response = response_list[0] ?: throw ErrorLoadingException("VideoSeyred") + val responseRaw = app.get(videoUrl) + val responseList:List = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") + val response = responseList[0] ?: throw ErrorLoadingException("VideoSeyred") for (track in response.tracks) { if (track.label != null && track.kind == "captions") { From 8fcb3e3121de93017f7b3cbf959b4429040a2184 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:45:25 -0600 Subject: [PATCH 139/157] Fix cast recycler scrolling (#1221) --- .../ui/result/ResultFragmentPhone.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) 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 f1399e8d..97bc49ea 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 @@ -23,6 +23,7 @@ import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import com.discord.panels.OverlappingPanelsLayout +import com.discord.panels.PanelState import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext @@ -118,6 +119,14 @@ open class ResultFragmentPhone : FullScreenPlayer() { return root } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { register(it) } + } + } + var currentTrailers: List = emptyList() var currentTrailerIndex = 0 @@ -210,9 +219,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } override fun onDestroyView() { - - //somehow this still leaks and I dont know why???? - // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt PanelsChildGestureRegionObserver.Provider.get().let { obs -> resultBinding?.resultCastItems?.let { obs.unregister(it) @@ -329,13 +335,18 @@ open class ResultFragmentPhone : FullScreenPlayer() { syncModel.addFromUrl(storedData.url) val api = APIHolder.getApiFromNameNull(storedData.apiName) - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { - register(it) + // This may not be 100% reliable, and may delay for small period + // before resultCastItems will be scrollable again, but this does work + // most of the time. + binding?.resultOverlappingPanels?.registerEndPanelStateListeners( + object : OverlappingPanelsLayout.PanelStateListener { + override fun onPanelStateChange(panelState: PanelState) { + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { register(it) } + } + } } - addGestureRegionsUpdateListener(gestureRegionsListener) - } - + ) // ===== ===== ===== @@ -674,6 +685,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { observe(viewModel.page) { data -> if (data == null) return@observe resultBinding?.apply { + PanelsChildGestureRegionObserver.Provider.get().apply { + register(resultCastItems) + } (data as? Resource.Success)?.value?.let { d -> resultVpn.setText(d.vpnText) resultInfo.setText(d.metaText) @@ -1167,4 +1181,4 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } } -} +} \ No newline at end of file From ab379ab31c30069aac3afdeec76410e69ea6bc95 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:54:54 -0600 Subject: [PATCH 140/157] Support for multi deleting downloads and other major improvements/fixes (#1177) --- .../lagradost/cloudstream3/MainActivity.kt | 51 +- .../ui/download/DownloadAdapter.kt | 386 +++++++++----- .../ui/download/DownloadButtonSetup.kt | 31 +- .../ui/download/DownloadChildFragment.kt | 174 +++++-- .../ui/download/DownloadFragment.kt | 182 +++++-- .../ui/download/DownloadViewModel.kt | 483 +++++++++++++++--- .../ui/download/button/BaseFetchButton.kt | 7 +- .../ui/download/button/PieFetchButton.kt | 6 +- .../ui/player/DownloadFileGenerator.kt | 32 +- .../ui/player/DownloadedPlayerActivity.kt | 11 +- .../ui/result/ResultTrailerPlayer.kt | 30 +- .../cloudstream3/utils/AppContextUtils.kt | 14 +- .../utils/BackPressedCallbackHelper.kt | 30 ++ .../cloudstream3/utils/SnackbarHelper.kt | 84 +++ .../cloudstream3/utils/SubtitleUtils.kt | 56 ++ .../utils/VideoDownloadManager.kt | 45 +- .../res/layout/download_child_episode.xml | 17 +- .../res/layout/download_header_episode.xml | 11 + .../res/layout/fragment_child_downloads.xml | 59 ++- .../main/res/layout/fragment_downloads.xml | 109 ++-- app/src/main/res/values/strings.xml | 12 +- 21 files changed, 1384 insertions(+), 446 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index b59265ee..5408d2a8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -133,6 +133,8 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback @@ -151,6 +153,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute @@ -1254,17 +1257,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa this.setKey(getString(R.string.jsdelivr_proxy_key), false) } else { this.setKey(getString(R.string.jsdelivr_proxy_key), true) - val parentView: View = findViewById(android.R.id.content) - Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG) - .let { snackbar -> - snackbar.setAction(R.string.revert) { - setKey(getString(R.string.jsdelivr_proxy_key), false) - } - snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground)) - snackbar.setTextColor(colorFromAttribute(R.attr.textColor)) - snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary)) - snackbar.show() - } + showSnackbar( + this@MainActivity, + R.string.jsdelivr_enabled, + Snackbar.LENGTH_LONG, + R.string.revert + ) { setKey(getString(R.string.jsdelivr_proxy_key), false) } } } } @@ -1603,7 +1601,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (isLayout(TV or EMULATOR)) { if (navDestination.matchDestination(R.id.navigation_home)) { - attachBackPressedCallback() + attachBackPressedCallback { + showConfirmExitDialog() + window?.navigationBarColor = + colorFromAttribute(R.attr.primaryGrayBackground) + updateLocale() + } } else detachBackPressedCallback() } } @@ -1848,28 +1851,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa finish() } - private var backPressedCallback: OnBackPressedCallback? = null - - private fun attachBackPressedCallback() { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - showConfirmExitDialog() - window?.navigationBarColor = - colorFromAttribute(R.attr.primaryGrayBackground) - updateLocale() - } - } - } - - backPressedCallback?.isEnabled = true - onBackPressedDispatcher.addCallback(this, backPressedCallback ?: return) - } - - private fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - } - suspend fun checkGithubConnectivity(): Boolean { return try { app.get( @@ -1880,4 +1861,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index 9a026334..20458429 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.ui.download -import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.CheckBox import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -31,47 +31,30 @@ const val DOWNLOAD_ACTION_LONG_CLICK = 5 const val DOWNLOAD_ACTION_GO_TO_CHILD = 0 const val DOWNLOAD_ACTION_LOAD_RESULT = 1 -abstract class VisualDownloadCached( - open val currentBytes: Long, - open val totalBytes: Long, - open val data: VideoDownloadHelper.DownloadCached -) { +sealed class VisualDownloadCached { + abstract val currentBytes: Long + abstract val totalBytes: Long + abstract val data: VideoDownloadHelper.DownloadCached + abstract var isSelected: Boolean - // Just to be extra-safe with areContentsTheSame - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is VisualDownloadCached) return false + data class Child( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadEpisodeCached, + override var isSelected: Boolean, + ) : VisualDownloadCached() - if (currentBytes != other.currentBytes) return false - if (totalBytes != other.totalBytes) return false - if (data != other.data) return false - - return true - } - - override fun hashCode(): Int { - var result = currentBytes.hashCode() - result = 31 * result + totalBytes.hashCode() - result = 31 * result + data.hashCode() - return result - } + data class Header( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadHeaderCached, + override var isSelected: Boolean, + val child: VideoDownloadHelper.DownloadEpisodeCached?, + val currentOngoingDownloads: Int, + val totalDownloads: Int, + ) : VisualDownloadCached() } -data class VisualDownloadChildCached( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadEpisodeCached, -): VisualDownloadCached(currentBytes, totalBytes, data) - -data class VisualDownloadHeaderCached( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadHeaderCached, - val child: VideoDownloadHelper.DownloadEpisodeCached?, - val currentOngoingDownloads: Int, - val totalDownloads: Int, -): VisualDownloadCached(currentBytes, totalBytes, data) - data class DownloadClickEvent( val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached @@ -83,108 +66,180 @@ data class DownloadHeaderClickEvent( ) class DownloadAdapter( - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, + private val onHeaderClickEvent: (DownloadHeaderClickEvent) -> Unit, + private val onItemClickEvent: (DownloadClickEvent) -> Unit, + private val onItemSelectionChanged: (Int, Boolean) -> Unit, ) : ListAdapter(DiffCallback()) { + private var isMultiDeleteState: Boolean = false + companion object { private const val VIEW_TYPE_HEADER = 0 private const val VIEW_TYPE_CHILD = 1 } inner class DownloadViewHolder( - private val binding: ViewBinding, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, + private val binding: ViewBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(card: VisualDownloadCached?) { when (binding) { - is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadHeaderCached) - is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadChildCached) + is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadCached.Header) + is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadCached.Child) } } - @SuppressLint("SetTextI18n") - private fun bindHeader(card: VisualDownloadHeaderCached?) { - if (binding !is DownloadHeaderEpisodeBinding) return - card ?: return - val d = card.data + private fun bindHeader(card: VisualDownloadCached.Header?) { + if (binding !is DownloadHeaderEpisodeBinding || card == null) return + val data = card.data binding.apply { - downloadHeaderPoster.apply { - setImage(d.poster) - setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_LOAD_RESULT, d)) + episodeHolder.apply { + if (isMultiDeleteState) { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true } } - downloadHeaderTitle.text = d.name - val formattedSizeString = formatShortFileSize(itemView.context, card.totalBytes) + downloadHeaderPoster.apply { + setImage(data.poster) + if (isMultiDeleteState) { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } else { + setOnClickListener { + onHeaderClickEvent.invoke( + DownloadHeaderClickEvent( + DOWNLOAD_ACTION_LOAD_RESULT, + data + ) + ) + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true + } + } + downloadHeaderTitle.text = data.name + val formattedSize = formatShortFileSize(itemView.context, card.totalBytes) if (card.child != null) { - downloadHeaderGotoChild.isVisible = false + handleChildDownload(card, formattedSize) + } else handleParentDownload(card, formattedSize) - val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) - if (status == DownloadStatusTell.IsDone) { - // We do this here instead if we are finished downloading - // so that we can use the value from the view model - // rather than extra unneeded disk operations and to prevent a - // delay in updating download icon state. - downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) - // We will let the view model handle this - downloadButton.doSetProgress = false - downloadButton.progressBar.progressDrawable = - downloadButton.getDrawableFromStatus(status) - ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadHeaderInfo.text = formattedSizeString - } else { - downloadButton.doSetProgress = true - downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + if (isMultiDeleteState) { + deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> + onItemSelectionChanged.invoke(data.id, isChecked) } + } else deleteCheckbox.setOnCheckedChangeListener(null) - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) - downloadButton.isVisible = true - - episodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) - } - } else { - downloadButton.isVisible = false - downloadHeaderGotoChild.isVisible = true - - try { - downloadHeaderInfo.text = downloadHeaderInfo.context.getString(R.string.extra_info_format) - .format( - card.totalDownloads, - downloadHeaderInfo.context.resources.getQuantityString( - R.plurals.episodes, - card.totalDownloads - ), - formattedSizeString - ) - } catch (e: Exception) { - // You probably formatted incorrectly - downloadHeaderInfo.text = "Error" - logError(e) - } - - episodeHolder.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_GO_TO_CHILD, d)) - } + deleteCheckbox.apply { + isVisible = isMultiDeleteState + isChecked = card.isSelected } } } - private fun bindChild(card: VisualDownloadChildCached?) { - if (binding !is DownloadChildEpisodeBinding) return - card ?: return - val d = card.data + private fun DownloadHeaderEpisodeBinding.handleChildDownload( + card: VisualDownloadCached.Header, + formattedSize: String + ) { + card.child ?: return + downloadHeaderGotoChild.isVisible = false + val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) + if (status == DownloadStatusTell.IsDone) { + // We do this here instead if we are finished downloading + // so that we can use the value from the view model + // rather than extra unneeded disk operations and to prevent a + // delay in updating download icon state. + downloadButton.setProgress(card.currentBytes, card.totalBytes) + downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) + // We will let the view model handle this + downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } + downloadHeaderInfo.text = formattedSize + } else { + // We need to make sure we restore the correct progress + // when we refresh data in the adapter. + downloadButton.resetView() + val drawable = downloadButton.getDrawableFromStatus(status)?.let { + ContextCompat.getDrawable(downloadButton.context, it) + } + downloadButton.statusView.setImageDrawable(drawable) + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable( + downloadButton.context, + downloadButton.progressDrawable + ) + } + + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, onItemClickEvent) + downloadButton.isVisible = !isMultiDeleteState + + if (!isMultiDeleteState) { + episodeHolder.setOnClickListener { + onItemClickEvent.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + card.child + ) + ) + } + } + } + + private fun DownloadHeaderEpisodeBinding.handleParentDownload( + card: VisualDownloadCached.Header, + formattedSize: String + ) { + downloadButton.isVisible = false + downloadHeaderGotoChild.isVisible = !isMultiDeleteState + + try { + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format).format( + card.totalDownloads, + downloadHeaderInfo.context.resources.getQuantityString( + R.plurals.episodes, + card.totalDownloads + ), + formattedSize + ) + } catch (e: Exception) { + downloadHeaderInfo.text = "" + logError(e) + } + + if (!isMultiDeleteState) { + episodeHolder.setOnClickListener { + onHeaderClickEvent.invoke( + DownloadHeaderClickEvent( + DOWNLOAD_ACTION_GO_TO_CHILD, + card.data + ) + ) + } + } + } + + private fun bindChild(card: VisualDownloadCached.Child?) { + if (binding !is DownloadChildEpisodeBinding || card == null) return + + val data = card.data binding.apply { - val posDur = getViewPos(d.id) + val posDur = getViewPos(data.id) downloadChildEpisodeProgress.apply { isVisible = posDur != null posDur?.let { @@ -194,36 +249,87 @@ class DownloadAdapter( } } - val status = downloadButton.getStatus(d.id, card.currentBytes, card.totalBytes) + val status = downloadButton.getStatus(data.id, card.currentBytes, card.totalBytes) if (status == DownloadStatusTell.IsDone) { // We do this here instead if we are finished downloading // so that we can use the value from the view model // rather than extra unneeded disk operations and to prevent a // delay in updating download icon state. downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes) + downloadButton.applyMetaData(data.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false downloadButton.progressBar.progressDrawable = downloadButton.getDrawableFromStatus(status) ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) + downloadChildEpisodeTextExtra.text = + formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) } else { - downloadButton.doSetProgress = true + // We need to make sure we restore the correct progress + // when we refresh data in the adapter. + downloadButton.resetView() + val drawable = downloadButton.getDrawableFromStatus(status)?.let { + ContextCompat.getDrawable(downloadButton.context, it) + } + downloadButton.statusView.setImageDrawable(drawable) downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + ContextCompat.getDrawable( + downloadButton.context, + downloadButton.progressDrawable + ) } - downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) - downloadButton.isVisible = true + downloadButton.setDefaultClickListener( + data, + downloadChildEpisodeTextExtra, + onItemClickEvent + ) + downloadButton.isVisible = !isMultiDeleteState downloadChildEpisodeText.apply { - text = context.getNameFull(d.name, d.episode, d.season) + text = context.getNameFull(data.name, data.episode, data.season) isSelected = true // Needed for text repeating } downloadChildEpisodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + onItemClickEvent.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, data)) + } + + downloadChildEpisodeHolder.apply { + when { + isMultiDeleteState -> { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } + + else -> { + setOnClickListener { + onItemClickEvent.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + data + ) + ) + } + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true + } + } + + if (isMultiDeleteState) { + deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> + onItemSelectionChanged.invoke(data.id, isChecked) + } + } else deleteCheckbox.setOnCheckedChangeListener(null) + + deleteCheckbox.apply { + isVisible = isMultiDeleteState + isChecked = card.isSelected } } } @@ -236,7 +342,7 @@ class DownloadAdapter( VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false) else -> throw IllegalArgumentException("Invalid view type") } - return DownloadViewHolder(binding, clickCallback, mediaClickCallback) + return DownloadViewHolder(binding) } override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { @@ -245,18 +351,52 @@ class DownloadAdapter( override fun getItemViewType(position: Int): Int { return when (getItem(position)) { - is VisualDownloadChildCached -> VIEW_TYPE_CHILD - is VisualDownloadHeaderCached -> VIEW_TYPE_HEADER + is VisualDownloadCached.Child -> VIEW_TYPE_CHILD + is VisualDownloadCached.Header -> VIEW_TYPE_HEADER else -> throw IllegalArgumentException("Invalid data type at position $position") } } + fun setIsMultiDeleteState(value: Boolean) { + if (isMultiDeleteState == value) return + isMultiDeleteState = value + notifyItemRangeChanged(0, itemCount) + } + + fun notifyAllSelected() { + currentList.indices.forEach { index -> + if (!currentList[index].isSelected) { + notifyItemChanged(index) + } + } + } + + fun notifySelectionStates() { + currentList.indices.forEach { index -> + if (currentList[index].isSelected) { + notifyItemChanged(index) + } + } + } + + private fun toggleIsChecked(checkbox: CheckBox, itemId: Int) { + val isChecked = !checkbox.isChecked + checkbox.isChecked = isChecked + onItemSelectionChanged.invoke(itemId, isChecked) + } + class DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + override fun areItemsTheSame( + oldItem: VisualDownloadCached, + newItem: VisualDownloadCached + ): Boolean { return oldItem.data.id == newItem.data.id } - override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + override fun areContentsTheSame( + oldItem: VisualDownloadCached, + newItem: VisualDownloadCached + ): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index c8c40e29..bf2c1b49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,11 +1,10 @@ package com.lagradost.cloudstream3.ui.download import android.content.DialogInterface -import android.widget.Toast import androidx.appcompat.app.AlertDialog +import com.google.android.material.snackbar.Snackbar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator @@ -14,9 +13,11 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE +import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import kotlinx.coroutines.MainScope object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { @@ -29,9 +30,15 @@ object DownloadButtonSetup { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) + VideoDownloadManager.deleteFilesAndUpdateSettings( + ctx, + setOf(id), + MainScope() + ) } + DialogInterface.BUTTON_NEGATIVE -> { + // Do nothing on cancel } } } @@ -56,11 +63,13 @@ object DownloadButtonSetup { } } } + DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> { VideoDownloadManager.downloadEvent.invoke( Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause) ) } + DOWNLOAD_ACTION_RESUME_DOWNLOAD -> { activity?.let { ctx -> if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) { @@ -79,6 +88,7 @@ object DownloadButtonSetup { } } } + DOWNLOAD_ACTION_LONG_CLICK -> { activity?.let { act -> val length = @@ -88,12 +98,15 @@ object DownloadButtonSetup { )?.fileLength ?: 0 if (length > 0) { - showToast(R.string.delete, Toast.LENGTH_LONG) - } else { - showToast(R.string.download, Toast.LENGTH_LONG) + showSnackbar( + act, + R.string.offline_file, + Snackbar.LENGTH_LONG + ) } } } + DOWNLOAD_ACTION_PLAY_FILE -> { activity?.let { act -> val info = @@ -119,7 +132,7 @@ object DownloadButtonSetup { id = click.data.id, parentId = click.data.parentId, - name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName + name = act.getString(R.string.downloaded_file), // click.data.name ?: keyInfo.displayName season = click.data.season, episode = click.data.episode, headerName = parent.name, @@ -132,7 +145,7 @@ object DownloadButtonSetup { ) ) ) - //R.id.global_to_navigation_player, PlayerFragment.newInstance( + // R.id.global_to_navigation_player, PlayerFragment.newInstance( // UriData( // info.path.toString(), // keyInfo.basePath, @@ -145,7 +158,7 @@ object DownloadButtonSetup { // click.data.season // ), // getViewPos(click.data.id)?.position ?: 0 - //) + // ) ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 03db948c..09c48a04 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -1,29 +1,33 @@ package com.lagradost.cloudstream3.ui.download import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding +import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.getKeys +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext class DownloadChildFragment : Fragment() { + private lateinit var downloadsViewModel: DownloadViewModel + private var binding: FragmentChildDownloadsBinding? = null + companion object { fun newInstance(headerName: String, folder: String): Bundle { return Bundle().apply { @@ -34,61 +38,54 @@ class DownloadChildFragment : Fragment() { } override fun onDestroyView() { - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } - downloadDeleteEventListener = null + detachBackPressedCallback() binding = null super.onDestroyView() } - private var binding: FragmentChildDownloadsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root } - private fun updateList(folder: String) = main { - context?.let { ctx -> - val data = withContext(Dispatchers.IO) { ctx.getKeys(folder) } - val eps = withContext(Dispatchers.IO) { - data.mapNotNull { key -> - context?.getKey(key) - }.mapNotNull { - val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) - ?: return@mapNotNull null - VisualDownloadChildCached( - currentBytes = info.fileLength, - totalBytes = info.totalBytes, - data = it, - ) - } - }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } - if (eps.isEmpty()) { - activity?.onBackPressedDispatcher?.onBackPressed() - return@main - } - - (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps) - } - } - - private var downloadDeleteEventListener: ((Int) -> Unit)? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + /** + * We never want to retain multi-delete state + * when navigating to downloads. Setting this state + * immediately can sometimes result in the observer + * not being notified in time to update the UI. + * + * By posting to the main looper, we ensure that this + * operation is executed after the view has been fully created + * and all initializations are completed, allowing the + * observer to properly receive and handle the state change. + */ + Handler(Looper.getMainLooper()).post { + downloadsViewModel.setIsMultiDeleteState(false) + } + + /** + * We have to make sure selected items are + * cleared here as well so we don't run in an + * inconsistent state where selected items do + * not match the multi delete state we are in. + */ + downloadsViewModel.clearSelectedItems() + val folder = arguments?.getString("folder") val name = arguments?.getString("name") if (folder == null) { - activity?.onBackPressedDispatcher?.onBackPressed() // TODO FIX + activity?.onBackPressedDispatcher?.onBackPressed() return } - fixPaddingStatusbar(binding?.downloadChildRoot) binding?.downloadChildToolbar?.apply { title = name @@ -101,13 +98,55 @@ class DownloadChildFragment : Fragment() { setAppBarNoScrollFlagsOnTV() } + binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() + + observe(downloadsViewModel.childCards) { + if (it.isEmpty()) { + activity?.onBackPressedDispatcher?.onBackPressed() + return@observe + } + + (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(it) + } + observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> + val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter + adapter?.setIsMultiDeleteState(isMultiDeleteState) + binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState + if (!isMultiDeleteState) { + detachBackPressedCallback() + downloadsViewModel.clearSelectedItems() + binding?.downloadChildToolbar?.isVisible = true + } + } + observe(downloadsViewModel.selectedBytes) { + updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) + } + observe(downloadsViewModel.selectedItemIds) { + handleSelectedChange(it) + updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) + + binding?.btnDelete?.isVisible = it.isNotEmpty() + binding?.selectItemsText?.isVisible = it.isEmpty() + + val allSelected = downloadsViewModel.isAllSelected() + if (allSelected) { + binding?.btnToggleAll?.setText(R.string.deselect_all) + } else binding?.btnToggleAll?.setText(R.string.select_all) + } + val adapter = DownloadAdapter( {}, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener(folder) - } + { click -> + if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.handleSingleDelete(ctx, click.data.id) + } + } else handleDownloadClick(click) + }, + { itemId, isChecked -> + if (isChecked) { + downloadsViewModel.addSelected(itemId) + } else downloadsViewModel.removeSelected(itemId) } ) @@ -122,18 +161,47 @@ class DownloadChildFragment : Fragment() { ) } - updateList(folder) + context?.let { downloadsViewModel.updateChildList(it, folder) } + fixPaddingStatusbar(binding?.downloadChildRoot) } - private fun setUpDownloadDeleteListener(folder: String) { - downloadDeleteEventListener = { id: Int -> - val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList - if (list != null) { - if (list.any { it.data.id == id }) { - updateList(folder) + private fun handleSelectedChange(selected: MutableSet) { + if (selected.isNotEmpty()) { + binding?.downloadDeleteAppbar?.isVisible = true + binding?.downloadChildToolbar?.isVisible = false + activity?.attachBackPressedCallback { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnDelete?.setOnClickListener { + context?.let { ctx -> + downloadsViewModel.handleMultiDelete(ctx) } } + + binding?.btnCancel?.setOnClickListener { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnToggleAll?.setOnClickListener { + val allSelected = downloadsViewModel.isAllSelected() + val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter + if (allSelected) { + adapter?.notifySelectionStates() + downloadsViewModel.clearSelectedItems() + } else { + adapter?.notifyAllSelected() + downloadsViewModel.selectAllItems() + } + } + + downloadsViewModel.setIsMultiDeleteState(true) } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateDeleteButton(count: Int, selectedBytes: Long) { + val formattedSize = formatShortFileSize(context, selectedBytes) + binding?.btnDelete?.text = + getString(R.string.delete_format).format(count, formattedSize) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 23d546e1..447b4f13 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -8,6 +8,8 @@ import android.content.Intent import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View @@ -17,7 +19,6 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged @@ -27,7 +28,7 @@ import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding import com.lagradost.cloudstream3.databinding.StreamInputBinding -import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -40,20 +41,22 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE -import com.lagradost.cloudstream3.utils.DataStore +import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { private lateinit var downloadsViewModel: DownloadViewModel + private var binding: FragmentDownloadsBinding? = null private fun View.setLayoutWidth(weight: Long) { val param = LinearLayout.LayoutParams( @@ -65,14 +68,11 @@ class DownloadFragment : Fragment() { } override fun onDestroyView() { - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } - downloadDeleteEventListener = null + detachBackPressedCallback() binding = null super.onDestroyView() } - private var binding: FragmentDownloadsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -84,12 +84,34 @@ class DownloadFragment : Fragment() { return localBinding.root } - private var downloadDeleteEventListener: ((Int) -> Unit)? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() + + /** + * We never want to retain multi-delete state + * when navigating to downloads. Setting this state + * immediately can sometimes result in the observer + * not being notified in time to update the UI. + * + * By posting to the main looper, we ensure that this + * operation is executed after the view has been fully created + * and all initializations are completed, allowing the + * observer to properly receive and handle the state change. + */ + Handler(Looper.getMainLooper()).post { + downloadsViewModel.setIsMultiDeleteState(false) + } + + /** + * We have to make sure selected items are + * cleared here as well so we don't run in an + * inconsistent state where selected items do + * not match the multi delete state we are in. + */ + downloadsViewModel.clearSelectedItems() observe(downloadsViewModel.headerCards) { (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it) @@ -97,25 +119,82 @@ class DownloadFragment : Fragment() { binding?.textNoDownloads?.isVisible = it.isEmpty() } observe(downloadsViewModel.availableBytes) { - updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree) + updateStorageInfo( + view.context, + it, + R.string.free_storage, + binding?.downloadFreeTxt, + binding?.downloadFree + ) } observe(downloadsViewModel.usedBytes) { - updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed) - binding?.downloadStorageAppbar?.isVisible = it > 0 + updateStorageInfo( + view.context, + it, + R.string.used_storage, + binding?.downloadUsedTxt, + binding?.downloadUsed + ) + + // Prevent race condition and make sure + // we don't display it early + if ( + downloadsViewModel.isMultiDeleteState.value == null || + downloadsViewModel.isMultiDeleteState.value == false + ) binding?.downloadStorageAppbar?.isVisible = it > 0 } observe(downloadsViewModel.downloadBytes) { - updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp) + updateStorageInfo( + view.context, + it, + R.string.app_storage, + binding?.downloadAppTxt, + binding?.downloadApp + ) + } + observe(downloadsViewModel.selectedBytes) { + updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) + } + observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> + val adapter = binding?.downloadList?.adapter as? DownloadAdapter + adapter?.setIsMultiDeleteState(isMultiDeleteState) + binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState + if (!isMultiDeleteState) { + detachBackPressedCallback() + downloadsViewModel.clearSelectedItems() + // Prevent race condition and make sure + // we don't display it early + if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) { + binding?.downloadStorageAppbar?.isVisible = true + } + } + } + observe(downloadsViewModel.selectedItemIds) { + handleSelectedChange(it) + updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) + + binding?.btnDelete?.isVisible = it.isNotEmpty() + binding?.selectItemsText?.isVisible = it.isEmpty() + + val allSelected = downloadsViewModel.isAllSelected() + if (allSelected) { + binding?.btnToggleAll?.setText(R.string.deselect_all) + } else binding?.btnToggleAll?.setText(R.string.select_all) } val adapter = DownloadAdapter( + { click -> handleItemClick(click) }, { click -> - handleItemClick(click) + if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.handleSingleDelete(ctx, click.data.id) + } + } else handleDownloadClick(click) }, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener() - } + { itemId, isChecked -> + if (isChecked) { + downloadsViewModel.addSelected(itemId) + } else downloadsViewModel.removeSelected(itemId) } ) @@ -126,7 +205,6 @@ class DownloadFragment : Fragment() { setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, - nextUp = FOCUS_SELF, nextDown = FOCUS_SELF, ) } @@ -147,35 +225,68 @@ class DownloadFragment : Fragment() { handleScroll(scrollY - oldScrollY) } } - downloadsViewModel.updateList(requireContext()) + + context?.let { downloadsViewModel.updateHeaderList(it) } fixPaddingStatusbar(binding?.downloadRoot) } private fun handleItemClick(click: DownloadHeaderClickEvent) { when (click.action) { DOWNLOAD_ACTION_GO_TO_CHILD -> { - if (!click.data.type.isMovieType()) { - val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) + if (click.data.type.isEpisodeBased()) { + val folder = + getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) activity?.navigate( R.id.action_navigation_downloads_to_navigation_download_child, DownloadChildFragment.newInstance(click.data.name, folder) ) } } + DOWNLOAD_ACTION_LOAD_RESULT -> { - (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName) + activity?.loadResult(click.data.url, click.data.apiName) } } } - private fun setUpDownloadDeleteListener() { - downloadDeleteEventListener = { id -> - val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList - if (list?.any { it.data.id == id } == true) { - context?.let { downloadsViewModel.updateList(it) } + private fun handleSelectedChange(selected: MutableSet) { + if (selected.isNotEmpty()) { + binding?.downloadDeleteAppbar?.isVisible = true + binding?.downloadStorageAppbar?.isVisible = false + activity?.attachBackPressedCallback { + downloadsViewModel.setIsMultiDeleteState(false) } + + binding?.btnDelete?.setOnClickListener { + context?.let { ctx -> + downloadsViewModel.handleMultiDelete(ctx) + } + } + + binding?.btnCancel?.setOnClickListener { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnToggleAll?.setOnClickListener { + val allSelected = downloadsViewModel.isAllSelected() + val adapter = binding?.downloadList?.adapter as? DownloadAdapter + if (allSelected) { + adapter?.notifySelectionStates() + downloadsViewModel.clearSelectedItems() + } else { + adapter?.notifyAllSelected() + downloadsViewModel.selectAllItems() + } + } + + downloadsViewModel.setIsMultiDeleteState(true) } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateDeleteButton(count: Int, selectedBytes: Long) { + val formattedSize = formatShortFileSize(context, selectedBytes) + binding?.btnDelete?.text = + getString(R.string.delete_format).format(count, formattedSize) } private fun updateStorageInfo( @@ -185,7 +296,10 @@ class DownloadFragment : Fragment() { textView: TextView?, view: View? ) { - textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes)) + textView?.text = getString(R.string.storage_size_format).format( + getString(stringRes), + formatShortFileSize(context, bytes) + ) view?.setLayoutWidth(bytes) } @@ -218,7 +332,9 @@ class DownloadFragment : Fragment() { if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) } - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy -> + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt( + 0 + )?.text?.toString()?.let { copy -> val fixedText = copy.trim() binding.streamUrl.setText(fixedText) activateSwitchOnHls(fixedText, binding) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 83d96592..137f1355 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -1,122 +1,439 @@ package com.lagradost.cloudstream3.ui.download import android.content.Context +import android.content.DialogInterface import android.os.Environment import android.os.StatFs +import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DownloadViewModel : ViewModel() { - private val _headerCards = - MutableLiveData>().apply { listOf() } - val headerCards: LiveData> = _headerCards + + private val _headerCards = MutableLiveData>() + val headerCards: LiveData> = _headerCards + + private val _childCards = MutableLiveData>() + val childCards: LiveData> = _childCards private val _usedBytes = MutableLiveData() - private val _availableBytes = MutableLiveData() - private val _downloadBytes = MutableLiveData() - val usedBytes: LiveData = _usedBytes + + private val _availableBytes = MutableLiveData() val availableBytes: LiveData = _availableBytes + + private val _downloadBytes = MutableLiveData() val downloadBytes: LiveData = _downloadBytes - private var previousVisual: List? = null + private val _selectedBytes = MutableLiveData(0) + val selectedBytes: LiveData = _selectedBytes - fun updateList(context: Context) = viewModelScope.launchSafe { - val children = withContext(Dispatchers.IO) { - context.getKeys(DOWNLOAD_EPISODE_CACHE) + private val _isMultiDeleteState = MutableLiveData(false) + val isMultiDeleteState: LiveData = _isMultiDeleteState + + private val _selectedItemIds = MutableLiveData>(mutableSetOf()) + val selectedItemIds: LiveData> = _selectedItemIds + + private var previousVisual: List? = null + + fun setIsMultiDeleteState(value: Boolean) { + _isMultiDeleteState.postValue(value) + } + + fun addSelected(itemId: Int) { + updateSelectedItems { it.add(itemId) } + } + + fun removeSelected(itemId: Int) { + updateSelectedItems { it.remove(itemId) } + } + + fun selectAllItems() { + val items = headerCards.value.orEmpty() + childCards.value.orEmpty() + updateSelectedItems { it.addAll(items.map { item -> item.data.id }) } + } + + fun clearSelectedItems() { + // We need this to be done immediately + // so we can't use postValue + _selectedItemIds.value = mutableSetOf() + updateSelectedItems { it.clear() } + } + + fun isAllSelected(): Boolean { + val currentSelected = selectedItemIds.value ?: return false + val items = headerCards.value.orEmpty() + childCards.value.orEmpty() + return items.count() == currentSelected.count() && items.all { it.data.id in currentSelected } + } + + private fun updateSelectedItems(action: (MutableSet) -> Unit) { + val currentSelected = selectedItemIds.value ?: mutableSetOf() + action(currentSelected) + _selectedItemIds.postValue(currentSelected) + updateSelectedBytes() + updateSelectedCards() + } + + private fun updateSelectedBytes() = viewModelScope.launchSafe { + val selectedItemsList = getSelectedItemsData() ?: return@launchSafe + val totalSelectedBytes = selectedItemsList.sumOf { it.totalBytes } + _selectedBytes.postValue(totalSelectedBytes) + } + + private fun updateSelectedCards() = viewModelScope.launchSafe { + val currentSelected = selectedItemIds.value ?: return@launchSafe + + headerCards.value?.let { headers -> + headers.forEach { header -> + header.isSelected = header.data.id in currentSelected + } + _headerCards.postValue(headers) + } + + childCards.value?.let { children -> + children.forEach { child -> + child.isSelected = child.data.id in currentSelected + } + _childCards.postValue(children) + } + } + + fun updateHeaderList(context: Context) = viewModelScope.launchSafe { + val visual = withContext(Dispatchers.IO) { + val children = context.getKeys(DOWNLOAD_EPISODE_CACHE) .mapNotNull { context.getKey(it) } .distinctBy { it.id } // Remove duplicates + + val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) = + calculateDownloadStats(context, children) + + val cached = context.getKeys(DOWNLOAD_HEADER_CACHE) + .mapNotNull { context.getKey(it) } + + createVisualDownloadList( + context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads + ) } - // parentId : bytes - val totalBytesUsedByChild = HashMap() - // parentId : bytes - val currentBytesUsedByChild = HashMap() - // parentId : downloadsCount - val totalDownloads = HashMap() - - // Gets all children downloads - withContext(Dispatchers.IO) { - children.forEach { c -> - val childFile = getDownloadFileInfoAndUpdateSettings(context, c.id) ?: return@forEach - - if (childFile.fileLength <= 1) return@forEach - val len = childFile.totalBytes - val flen = childFile.fileLength - - totalBytesUsedByChild[c.parentId] = totalBytesUsedByChild[c.parentId]?.plus(len) ?: len - currentBytesUsedByChild[c.parentId] = currentBytesUsedByChild[c.parentId]?.plus(flen) ?: flen - totalDownloads[c.parentId] = totalDownloads[c.parentId]?.plus(1) ?: 1 - } - } - - val cached = withContext(Dispatchers.IO) { // Won't fetch useless keys - totalDownloads.entries.filter { it.value > 0 }.mapNotNull { - context.getKey( - DOWNLOAD_HEADER_CACHE, - it.key.toString() - ) - } - } - - val visual = withContext(Dispatchers.IO) { - cached.mapNotNull { - val downloads = totalDownloads[it.id] ?: 0 - val bytes = totalBytesUsedByChild[it.id] ?: 0 - val currentBytes = currentBytesUsedByChild[it.id] ?: 0 - if (bytes <= 0 || downloads <= 0) return@mapNotNull null - val movieEpisode = - if (!it.type.isMovieType()) null - else context.getKey( - DOWNLOAD_EPISODE_CACHE, - getFolderName(it.id.toString(), it.id.toString()) - ) - VisualDownloadHeaderCached( - currentBytes = currentBytes, - totalBytes = bytes, - data = it, - child = movieEpisode, - currentOngoingDownloads = 0, - totalDownloads = downloads, - ) - }.sortedBy { - (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) - } // Episode sorting by episode, lowest to highest - } - - // Only update list if different from the previous one to prevent duplicate initialization if (visual != previousVisual) { previousVisual = visual - - try { - val stat = StatFs(Environment.getExternalStorageDirectory().path) - val localBytesAvailable = stat.availableBytes - val localTotalBytes = stat.blockSizeLong * stat.blockCountLong - val localDownloadedBytes = visual.sumOf { it.totalBytes } - - _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) - _availableBytes.postValue(localBytesAvailable) - _downloadBytes.postValue(localDownloadedBytes) - } catch (t: Throwable) { - _downloadBytes.postValue(0) - logError(t) - } - + updateStorageStats(visual) _headerCards.postValue(visual) } } + + private fun calculateDownloadStats( + context: Context, + children: List + ): Triple, Map, Map> { + // parentId : bytes + val totalBytesUsedByChild = mutableMapOf() + // parentId : bytes + val currentBytesUsedByChild = mutableMapOf() + // parentId : downloadsCount + val totalDownloads = mutableMapOf() + + children.forEach { child -> + val childFile = getDownloadFileInfoAndUpdateSettings(context, child.id) ?: return@forEach + if (childFile.fileLength <= 1) return@forEach + + val len = childFile.totalBytes + val flen = childFile.fileLength + + totalBytesUsedByChild.merge(child.parentId, len, Long::plus) + currentBytesUsedByChild.merge(child.parentId, flen, Long::plus) + totalDownloads.merge(child.parentId, 1, Int::plus) + } + return Triple(totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) + } + + private fun createVisualDownloadList( + context: Context, + cached: List, + totalBytesUsedByChild: Map, + currentBytesUsedByChild: Map, + totalDownloads: Map + ): List { + return cached.mapNotNull { + val downloads = totalDownloads[it.id] ?: 0 + val bytes = totalBytesUsedByChild[it.id] ?: 0 + val currentBytes = currentBytesUsedByChild[it.id] ?: 0 + if (bytes <= 0 || downloads <= 0) return@mapNotNull null + + val isSelected = selectedItemIds.value?.contains(it.id) ?: false + val movieEpisode = if (it.type.isEpisodeBased()) null else context.getKey( + DOWNLOAD_EPISODE_CACHE, + getFolderName(it.id.toString(), it.id.toString()) + ) + + VisualDownloadCached.Header( + currentBytes = currentBytes, + totalBytes = bytes, + data = it, + child = movieEpisode, + currentOngoingDownloads = 0, + totalDownloads = downloads, + isSelected = isSelected, + ) + // Prevent order being almost completely random, + // making things difficult to find. + }.sortedWith(compareBy { + // Sort by isEpisodeBased() ascending. We put those that + // are episode based at the bottom for UI purposes and to + // make it easier to find by grouping them together. + it.data.type.isEpisodeBased() + }.thenBy { + // Then we sort alphabetically by name (case-insensitive). + // Again, we do this to make things easier to find. + it.data.name.lowercase() + }) + } + + fun updateChildList(context: Context, folder: String) = viewModelScope.launchSafe { + val visual = withContext(Dispatchers.IO) { + context.getKeys(folder).mapNotNull { key -> + context.getKey(key) + }.mapNotNull { + val isSelected = selectedItemIds.value?.contains(it.id) ?: false + val info = getDownloadFileInfoAndUpdateSettings(context, it.id) ?: return@mapNotNull null + VisualDownloadCached.Child( + currentBytes = info.fileLength, + totalBytes = info.totalBytes, + isSelected = isSelected, + data = it, + ) + } + }.sortedWith(compareBy( + // Sort by season first, and then by episode number, + // to ensure sorting is consistent. + { it.data.season ?: 0 }, + { it.data.episode } + )) + + if (previousVisual != visual) { + previousVisual = visual + _childCards.postValue(visual) + } + } + + private fun removeItems(idsToRemove: Set) = viewModelScope.launchSafe { + val updatedHeaders = headerCards.value.orEmpty().filter { it.data.id !in idsToRemove } + val updatedChildren = childCards.value.orEmpty().filter { it.data.id !in idsToRemove } + _headerCards.postValue(updatedHeaders) + _childCards.postValue(updatedChildren) + } + + private fun updateStorageStats(visual: List) { + try { + val stat = StatFs(Environment.getExternalStorageDirectory().path) + val localBytesAvailable = stat.availableBytes + val localTotalBytes = stat.blockSizeLong * stat.blockCountLong + val localDownloadedBytes = visual.sumOf { it.totalBytes } + val localUsedBytes = localTotalBytes - localBytesAvailable + _usedBytes.postValue(localUsedBytes) + _availableBytes.postValue(localBytesAvailable) + _downloadBytes.postValue(localDownloadedBytes) + } catch (t: Throwable) { + _downloadBytes.postValue(0) + logError(t) + } + } + + fun handleMultiDelete(context: Context) = viewModelScope.launchSafe { + val selectedItemsList = getSelectedItemsData().orEmpty() + val deleteData = processSelectedItems(context, selectedItemsList) + val message = buildDeleteMessage(context, deleteData) + showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) + } + + fun handleSingleDelete( + context: Context, + itemId: Int + ) = viewModelScope.launchSafe { + val itemData = getItemDataFromId(itemId) + val deleteData = processSelectedItems(context, itemData) + val message = buildDeleteMessage(context, deleteData) + showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) + } + + private fun processSelectedItems( + context: Context, + selectedItemsList: List + ): DeleteData { + val names = mutableListOf() + val seriesNames = mutableListOf() + + val ids = mutableSetOf() + val parentIds = mutableSetOf() + + var parentName: String? = null + + selectedItemsList.forEach { item -> + when (item) { + is VisualDownloadCached.Header -> { + if (item.data.type.isEpisodeBased()) { + val episodes = context.getKeys(DOWNLOAD_EPISODE_CACHE) + .mapNotNull { + context.getKey( + it + ) + } + .filter { it.parentId == item.data.id } + .map { it.id } + ids.addAll(episodes) + parentIds.add(item.data.id) + + val episodeInfo = "${item.data.name} (${item.totalDownloads} ${ + context.resources.getQuantityString( + R.plurals.episodes, + item.totalDownloads + ).lowercase() + })" + seriesNames.add(episodeInfo) + } else { + ids.add(item.data.id) + names.add(item.data.name) + } + } + + is VisualDownloadCached.Child -> { + ids.add(item.data.id) + val parent = context.getKey( + DOWNLOAD_HEADER_CACHE, + item.data.parentId.toString() + ) + parentName = parent?.name + names.add( + context.getNameFull( + item.data.name, + item.data.episode, + item.data.season + ) + ) + } + } + } + + return DeleteData(ids, parentIds, seriesNames, names, parentName) + } + + private fun buildDeleteMessage( + context: Context, + data: DeleteData + ): String { + val formattedNames = data.names.sortedBy { it.lowercase() } + .joinToString(separator = "\n") { "• $it" } + val formattedSeriesNames = data.seriesNames.sortedBy { it.lowercase() } + .joinToString(separator = "\n") { "• $it" } + + return when { + data.ids.count() == 1 -> { + context.getString(R.string.delete_message).format( + data.names.firstOrNull() + ) + } + + data.seriesNames.isNotEmpty() && data.names.isEmpty() -> { + context.getString(R.string.delete_message_series_only).format(formattedSeriesNames) + } + + data.parentName != null && data.names.isNotEmpty() -> { + context.getString(R.string.delete_message_series_episodes) + .format(data.parentName, formattedNames) + } + + data.seriesNames.isNotEmpty() -> { + val seriesSection = context.getString(R.string.delete_message_series_section) + .format(formattedSeriesNames) + context.getString(R.string.delete_message_multiple) + .format(formattedNames) + "\n\n" + seriesSection + } + + else -> context.getString(R.string.delete_message_multiple).format(formattedNames) + } + } + + private fun showDeleteConfirmationDialog( + context: Context, + message: String, + ids: Set, + parentIds: Set + ) { + val builder = AlertDialog.Builder(context) + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + viewModelScope.launchSafe { + setIsMultiDeleteState(false) + deleteFilesAndUpdateSettings(context, ids, this) { successfulIds -> + // We always remove parent because if we are deleting from here + // and we have it as non-empty, it was triggered on + // parent header card + removeItems(successfulIds + parentIds) + } + } + } + + DialogInterface.BUTTON_NEGATIVE -> { + // Do nothing on cancel + } + } + } + + try { + val title = if (ids.count() == 1) { + R.string.delete_file + } else R.string.delete_files + builder.setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } catch (e: Exception) { + logError(e) + } + } + + private fun getSelectedItemsData(): List? { + val headers = headerCards.value.orEmpty() + val children = childCards.value.orEmpty() + + return selectedItemIds.value?.mapNotNull { id -> + headers.find { it.data.id == id } ?: children.find { it.data.id == id } + } + } + + private fun getItemDataFromId(itemId: Int): List { + val headers = headerCards.value.orEmpty() + val children = childCards.value.orEmpty() + + return (headers + children).filter { it.data.id == itemId } + } + + private data class DeleteData( + val ids: Set, + val parentIds: Set, + val seriesNames: List, + val names: List, + val parentName: String? + ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 45132131..908e3a80 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -93,7 +93,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : abstract fun setStatus(status: VideoDownloadManager.DownloadType?) - fun getStatus(id:Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { + fun getStatus(id: Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { // some extra padding for just in case return VideoDownloadManager.downloadStatus[id] ?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) { @@ -101,7 +101,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } else DownloadStatusTell.IsPaused } - fun applyMetaData(id:Int, downloadedBytes: Long, totalBytes: Long) { + fun applyMetaData(id: Int, downloadedBytes: Long, totalBytes: Long) { val status = getStatus(id, downloadedBytes, totalBytes) currentMetaData.apply { @@ -140,7 +140,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } else { if (doSetProgress) { progressText?.apply { - val currentFormattedSizeString = formatShortFileSize(context, downloadedBytes) + val currentFormattedSizeString = + formatShortFileSize(context, downloadedBytes) val totalFormattedSizeString = formatShortFileSize(context, totalBytes) text = // if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index abc159d0..29c2daa2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -58,7 +58,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } private var progressBarBackground: View - private var statusView: ImageView + var statusView: ImageView open fun onInflate() {} @@ -248,7 +248,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } */ @MainThread - private fun setStatusInternal(status : DownloadStatusTell?) { + private fun setStatusInternal(status: DownloadStatusTell?) { val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { val animation = AnimationUtils.loadAnimation(context, waitingAnimation) @@ -286,7 +286,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : if (Looper.myLooper() == Looper.getMainLooper()) { try { setStatusInternal(status) - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) // Just in case setStatusInternal throws because thread progressBarBackground.post { setStatusInternal(status) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index a8a3106a..a0668abc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -4,7 +4,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.SubtitleUtils.cleanDisplayName +import com.lagradost.cloudstream3.utils.SubtitleUtils.isMatchingSubtitle +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder import kotlin.math.max import kotlin.math.min @@ -49,10 +51,6 @@ class DownloadFileGenerator( return null } - private fun cleanDisplayName(name: String): String { - return name.substringBeforeLast('.').trim() - } - override suspend fun generateLinks( clearCache: Boolean, type: LoadType, @@ -69,28 +67,9 @@ class DownloadFileGenerator( val cleanDisplay = cleanDisplayName(display) - VideoDownloadManager.getFolder(ctx, relative, meta.basePath) - ?.forEach { (name, uri) -> - // only these files are allowed, so no videos as subtitles - if (listOf( - ".vtt", - ".srt", - ".txt", - ".ass", - ".ttml", - ".sbv", - ".dfxp" - ).none { name.contains(it, true) } - ) return@forEach - - // cant have the exact same file as a subtitle - if (name.equals(display, true)) return@forEach - + getFolder(ctx, relative, meta.basePath)?.forEach { (name, uri) -> + if (isMatchingSubtitle(name, display, cleanDisplay)) { val cleanName = cleanDisplayName(name) - - // we only want files with the approx same name - if (!cleanName.startsWith(cleanDisplay, true)) return@forEach - val realName = cleanName.removePrefix(cleanDisplay) subtitleCallback( @@ -104,6 +83,7 @@ class DownloadFileGenerator( ) ) } + } return true } 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 4279b542..c38160c2 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 @@ -4,13 +4,13 @@ import android.content.Intent import android.os.Bundle import android.util.Log import android.view.KeyEvent -import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" @@ -70,14 +70,7 @@ class DownloadedPlayerActivity : AppCompatActivity() { return } - onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finish() - } - } - ) + attachBackPressedCallback { finish() } } override fun onResume() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 135dc530..2ab60c2f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.activity.OnBackPressedCallback import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight @@ -17,6 +16,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback open class ResultTrailerPlayer : ResultFragmentPhone() { @@ -156,7 +157,9 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { uiReset() if (isFullScreenPlayer) { - attachBackPressedCallback() + activity?.attachBackPressedCallback { + updateFullscreen(false) + } } else detachBackPressedCallback() } @@ -175,27 +178,4 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { fixPlayerSize() } } - - private var backPressedCallback: OnBackPressedCallback? = null - - private fun attachBackPressedCallback() { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - updateFullscreen(false) - } - } - } - - backPressedCallback?.isEnabled = true - - activity?.onBackPressedDispatcher?.addCallback( - activity ?: return, - backPressedCallback ?: return - ) - } - - private fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index b13de062..8d65acf7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -677,9 +677,15 @@ object AppContextUtils { } fun Context.isNetworkAvailable(): Boolean { - val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val activeNetworkInfo = manager.activeNetworkInfo - return activeNetworkInfo != null && activeNetworkInfo.isConnected || manager.allNetworkInfo?.any { it.isConnected } ?: false + val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork ?: return false + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } else { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo?.isConnected == true + } } fun splitQuery(url: URL): Map { @@ -1018,4 +1024,4 @@ object AppContextUtils { } return currentAudioFocusRequest } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt new file mode 100644 index 00000000..1326ab27 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt @@ -0,0 +1,30 @@ +package com.lagradost.cloudstream3.utils + +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback + +object BackPressedCallbackHelper { + private var backPressedCallback: OnBackPressedCallback? = null + + fun ComponentActivity.attachBackPressedCallback(callback: () -> Unit) { + if (backPressedCallback == null) { + backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + callback.invoke() + } + } + } + + backPressedCallback?.isEnabled = true + + onBackPressedDispatcher.addCallback( + this@attachBackPressedCallback, + backPressedCallback ?: return + ) + } + + fun detachBackPressedCallback() { + backPressedCallback?.isEnabled = false + backPressedCallback = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt new file mode 100644 index 00000000..e6a77795 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt @@ -0,0 +1,84 @@ +package com.lagradost.cloudstream3.utils + +import android.app.Activity +import android.view.View +import androidx.annotation.MainThread +import androidx.annotation.StringRes +import com.google.android.material.snackbar.Snackbar +import com.lagradost.api.Log +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.result.UiText +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute + +object SnackbarHelper { + + private const val TAG = "COMPACT" + private var currentSnackbar: Snackbar? = null + + @MainThread + fun showSnackbar( + act: Activity?, + message: UiText, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: UiText? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null) return + showSnackbar(act, message.asString(act), duration, + actionText?.asString(act), actionCallback) + } + + @MainThread + fun showSnackbar( + act: Activity?, + @StringRes message: Int, + duration: Int = Snackbar.LENGTH_SHORT, + @StringRes actionText: Int? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null) return + showSnackbar(act, act.getString(message), duration, + actionText?.let { act.getString(it) }, actionCallback) + } + + @MainThread + fun showSnackbar( + act: Activity?, + message: String?, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: String? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null || message == null) { + Log.w(TAG, "Invalid showSnackbar: act = $act, message = $message") + return + } + Log.i(TAG, "showSnackbar: $message") + + try { + currentSnackbar?.dismiss() + } catch (e: Exception) { + logError(e) + } + + try { + val parentView = act.findViewById(android.R.id.content) + val snackbar = Snackbar.make(parentView, message, duration) + + actionCallback?.let { + snackbar.setAction(actionText) { actionCallback.invoke() } + } + + snackbar.show() + currentSnackbar = snackbar + + snackbar.setBackgroundTint(act.colorFromAttribute(R.attr.primaryBlackBackground)) + snackbar.setTextColor(act.colorFromAttribute(R.attr.textColor)) + snackbar.setActionTextColor(act.colorFromAttribute(R.attr.colorPrimary)) + + } catch (e: Exception) { + logError(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt new file mode 100644 index 00000000..93a53395 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.utils + +import android.content.Context +import com.lagradost.api.Log +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder +import com.lagradost.safefile.SafeFile + +object SubtitleUtils { + + // Only these files are allowed, so no videos as subtitles + private val allowedExtensions = listOf( + ".vtt", ".srt", ".txt", ".ass", + ".ttml", ".sbv", ".dfxp" + ) + + fun deleteMatchingSubtitles(context: Context, info: VideoDownloadManager.DownloadedFileInfo) { + val relative = info.relativePath + val display = info.displayName + val cleanDisplay = cleanDisplayName(display) + + getFolder(context, relative, info.basePath)?.forEach { (name, uri) -> + if (isMatchingSubtitle(name, display, cleanDisplay)) { + val subtitleFile = SafeFile.fromUri(context, uri) + if (subtitleFile == null || !subtitleFile.delete()) { + Log.e("SubtitleDeletion", "Failed to delete subtitle file: ${subtitleFile?.name()}") + } + } + } + } + + /** + * @param name the file name of the subtitle + * @param display the file name of the video + * @param cleanDisplay the cleanDisplayName of the video file name + */ + fun isMatchingSubtitle( + name: String, + display: String, + cleanDisplay: String + ): Boolean { + // Check if the file has a valid subtitle extension + val hasValidExtension = allowedExtensions.any { name.contains(it, ignoreCase = true) } + + // We can't have the exact same file as a subtitle + val isNotDisplayName = !name.equals(display, ignoreCase = true) + + // Check if the file name starts with a cleaned version of the display name + val startsWithCleanDisplay = cleanDisplayName(name).startsWith(cleanDisplay, ignoreCase = true) + + return hasValidExtension && isNotDisplayName && startsWithCleanDisplay + } + + fun cleanDisplayName(name: String): String { + return name.substringBeforeLast('.').trim() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index a3f6d789..2190e03f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -20,6 +20,7 @@ import androidx.work.WorkManager import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.api.Log import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -29,12 +30,14 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey +import com.lagradost.cloudstream3.utils.SubtitleUtils.deleteMatchingSubtitles import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.safefile.MediaFileContentType import com.lagradost.safefile.SafeFile @@ -42,6 +45,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -1733,7 +1738,37 @@ object VideoDownloadManager { } } - fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { + fun deleteFilesAndUpdateSettings( + context: Context, + ids: Set, + scope: CoroutineScope, + onComplete: (Set) -> Unit = {} + ) { + scope.launchSafe(Dispatchers.IO) { + val deleteJobs = ids.map { id -> + async { + id to deleteFileAndUpdateSettings(context, id) + } + } + val results = deleteJobs.awaitAll() + + val (successfulResults, failedResults) = results.partition { it.second } + val successfulIds = successfulResults.map { it.first }.toSet() + + if (failedResults.isNotEmpty()) { + failedResults.forEach { (id, _) -> + // TODO show a toast if some failed? + Log.e("FileDeletion", "Failed to delete file with ID: $id") + } + } else { + Log.i("FileDeletion", "All files deleted successfully") + } + + onComplete.invoke(successfulIds) + } + } + + private fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success @@ -1759,11 +1794,17 @@ object VideoDownloadManager { private fun deleteFile(context: Context, id: Int): Boolean { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false + val file = info.toFile(context) + downloadEvent.invoke(id to DownloadActionType.Stop) downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) - return info.toFile(context)?.delete() ?: false + + val isFileDeleted = file?.delete() == true || file?.exists() == false + if (isFileDeleted) deleteMatchingSubtitles(context, info) + + return isFileDeleted } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index 4974a027..e53e63d3 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -2,10 +2,8 @@ @@ -78,7 +73,6 @@ tools:text="128MB / 237MB" /> - + + \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index a0b64ce3..957869d4 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -77,5 +77,16 @@ android:focusable="true" android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_child_downloads.xml b/app/src/main/res/layout/fragment_child_downloads.xml index 9afaea0b..64ed1d70 100644 --- a/app/src/main/res/layout/fragment_child_downloads.xml +++ b/app/src/main/res/layout/fragment_child_downloads.xml @@ -7,13 +7,69 @@ android:layout_height="match_parent" android:background="?attr/primaryGrayBackground" android:orientation="vertical" - tools:context=".ui.download.DownloadFragment"> + tools:context=".ui.download.DownloadChildFragment"> + + + + +