diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 73c53292..e0e4bb1d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,14 +50,15 @@ android { } } - // https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading - compileSdk = 34 // android 14 is fucked + compileSdk = 34 buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + + // https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading + targetSdk = 33 // android 14 is fucked versionCode = 62 versionName = "4.2.1" @@ -157,16 +158,14 @@ dependencies { implementation("androidx.test.ext:junit-ktx:1.1.5") testImplementation("org.json:json:20230618") - implementation("androidx.core:core-ktx:1.12.0") // need 34 for higher - implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0 + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") - // dont change this to 1.6.0 it looks ugly af - implementation("com.google.android.material:material:1.5.0") + implementation("com.google.android.material:material:1.10.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - // need 34 for higher - implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") - implementation("androidx.navigation:navigation-ui-ktx:2.7.4") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") + implementation("androidx.navigation:navigation-ui-ktx:2.7.5") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") @@ -180,7 +179,7 @@ dependencies { // DONT UPDATE, WILL CRASH ANDROID TV ???? implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") - implementation("androidx.preference:preference-ktx:1.2.0") + implementation("androidx.preference:preference-ktx:1.2.1") implementation("com.github.bumptech.glide:glide:4.13.1") kapt("com.github.bumptech.glide:compiler:4.13.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e0d43338..453c1fae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -96,12 +96,6 @@ android:launchMode="singleTask" android:resizeableActivity="true" android:supportsPictureInPicture="true"> - - - - - - @@ -165,6 +159,22 @@ + + + + + + + + + + + + diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 5df7b1f6..35a628a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1246,6 +1246,18 @@ interface LoadResponse { return this.syncData[aniListIdPrefix] } + fun LoadResponse.getImdbId(): String? { + return normalSafeApiCall { + SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb) + } + } + + fun LoadResponse.getTMDbId(): String? { + return normalSafeApiCall { + SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb) + } + } + fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a4d1c7b4..a41028bd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1306,7 +1306,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it]) + viewModel.updateWatchStatus(WatchType.values()[it], this@MainActivity) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt deleted file mode 100644 index 8cfe1e9a..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.lagradost.cloudstream3.metaproviders - -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId -import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.SyncAPI -import com.lagradost.cloudstream3.syncproviders.providers.AniListApi -import com.lagradost.cloudstream3.syncproviders.providers.MALApi -import com.lagradost.cloudstream3.utils.SyncUtil - -// wont be implemented -class MultiAnimeProvider : MainAPI() { - override var name = "MultiAnime" - override var lang = "en" - override val usesWebView = true - override val supportedTypes = setOf(TvType.Anime) - private val syncApi: SyncAPI = aniListApi - - private val syncUtilType by lazy { - when (syncApi) { - is AniListApi -> "anilist" - is MALApi -> "myanimelist" - else -> throw ErrorLoadingException("Invalid Api") - } - } - - private val validApis - get() = - synchronized(APIHolder.apis) { - APIHolder.apis.filter { - it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains( - TvType.Anime - ) - } - } - - - private fun filterName(name: String): String { - return Regex("""[^a-zA-Z0-9-]""").replace(name, "") - } - - override suspend fun search(query: String): List? { - return syncApi.search(query)?.map { - AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl) - } - } - - override suspend fun load(url: String): LoadResponse? { - return syncApi.getResult(url)?.let { res -> - val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url -> - validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url) - }.filterNotNull() - - val type = - if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime - - newAnimeLoadResponse( - res.title ?: throw ErrorLoadingException("No Title found"), - url, - type - ) { - posterUrl = res.posterUrl - plot = res.synopsis - tags = res.genres - rating = res.publicScore - addTrailer(res.trailers) - addAniListId(res.id.toIntOrNull()) - recommendations = res.recommendations - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 36932396..3a37a228 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -203,7 +203,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } /** Read the id string to get all other ids */ - private fun readIdFromString(idString: String?): Map { + fun readIdFromString(idString: String?): Map { return tryParseJson(idString) ?: return emptyMap() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt index 6b3090a9..c5c38dc0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt @@ -55,7 +55,6 @@ class WhoIsWatchingAdapter( editCallBack = editCallBack, ) - override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) = holder.bind(currentList.getOrNull(position)) @@ -70,10 +69,15 @@ class WhoIsWatchingAdapter( fun bind(card: DataStoreHelper.Account?) { when (binding) { is WhoIsWatchingAccountBinding -> binding.apply { - if(card == null) return@apply + if (card == null) return@apply outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex profileText.text = card.name profileImageBackground.setImage(card.image) + + // Handle the lock indicator + val isLocked = card.lockPin != null + lockIcon.isVisible = isLocked + root.setOnClickListener { selectCallBack(card) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt new file mode 100644 index 00000000..aea55392 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.ui.account + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.databinding.AccountListItemBinding +import com.lagradost.cloudstream3.ui.result.setImage +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.DataStoreHelper + +class AccountAdapter( + private val accounts: List, + private val onItemClick: (DataStoreHelper.Account) -> Unit +) : RecyclerView.Adapter() { + + inner class AccountViewHolder(private val binding: AccountListItemBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(account: DataStoreHelper.Account) { + val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex + + binding.accountName.text = account.name + binding.accountImage.setImage(account.image) + binding.lockIcon.isVisible = account.lockPin != null + binding.outline.isVisible = isLastUsedAccount + + if (isTvSettings()) { + binding.root.isFocusableInTouchMode = true + if (isLastUsedAccount) { + binding.root.requestFocus() + } + } + + binding.root.setOnClickListener { + onItemClick(account) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { + val binding = AccountListItemBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + + return AccountViewHolder(binding) + } + + override fun onBindViewHolder(holder: AccountViewHolder, position: Int) { + holder.bind(accounts[position]) + } + + override fun getItemCount(): Int { + return accounts.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountDialog.kt new file mode 100644 index 00000000..76686aef --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountDialog.kt @@ -0,0 +1,132 @@ +package com.lagradost.cloudstream3.ui.account + +import android.content.Context +import android.view.LayoutInflater +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.LockPinDialogBinding +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod + +object AccountDialog { + // TODO add account creation dialog to allow creating accounts directly from AccountSelectActivity + + fun showPinInputDialog( + context: Context, + currentPin: String?, + editAccount: Boolean, + errorText: String? = null, + callback: (String?) -> Unit + ) { + fun TextView.visibleWithText(@StringRes textRes: Int) { + isVisible = true + setText(textRes) + } + + fun TextView.visibleWithText(text: String?) { + isVisible = true + setText(text) + } + + val binding = LockPinDialogBinding.inflate(LayoutInflater.from(context)) + + val isPinSet = currentPin != null + val isNewPin = editAccount && !isPinSet + val isEditPin = editAccount && isPinSet + + val titleRes = if (isEditPin) R.string.enter_current_pin else R.string.enter_pin + + var isPinValid = false + + val builder = AlertDialog.Builder(context, R.style.AlertDialogCustom) + .setView(binding.root) + .setTitle(titleRes) + .setNegativeButton(R.string.cancel) { _, _ -> + callback.invoke(null) + } + .setOnCancelListener { + callback.invoke(null) + } + .setOnDismissListener { + if (!isPinValid) { + callback.invoke(null) + } + } + + if (isNewPin) { + if (errorText != null) binding.pinEditTextError.visibleWithText(errorText) + builder.setPositiveButton(R.string.setup_done) { _, _ -> + if (!isPinValid) { + // If the done button is pressed and there is an error, + // ask again, and mention the error that caused this. + showPinInputDialog( + context = binding.root.context, + currentPin = null, + editAccount = true, + errorText = binding.pinEditTextError.text.toString(), + callback = callback + ) + } else { + val enteredPin = binding.pinEditText.text.toString() + callback.invoke(enteredPin) + } + } + } + + val dialog = builder.create() + + binding.pinEditText.doOnTextChanged { text, _, _, _ -> + val enteredPin = text.toString() + val isEnteredPinValid = enteredPin.length == 4 + + if (isEnteredPinValid) { + if (isPinSet) { + if (enteredPin != currentPin) { + binding.pinEditTextError.visibleWithText(R.string.pin_error_incorrect) + binding.pinEditText.text = null + isPinValid = false + } else { + binding.pinEditTextError.isVisible = false + isPinValid = true + + callback.invoke(enteredPin) + dialog.dismissSafe() + } + } else { + binding.pinEditTextError.isVisible = false + isPinValid = true + } + } else if (isNewPin) { + binding.pinEditTextError.visibleWithText(R.string.pin_error_length) + isPinValid = false + } + } + + // Detect IME_ACTION_DONE + binding.pinEditText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE && isPinValid) { + val enteredPin = binding.pinEditText.text.toString() + callback.invoke(enteredPin) + dialog.dismissSafe() + } + true + } + + // We don't want to accidentally have the dialog dismiss when clicking outside of it. + // That is what the cancel button is for. + dialog.setCanceledOnTouchOutside(false) + + dialog.show() + + // Auto focus on PIN input and show keyboard + binding.pinEditText.requestFocus() + binding.pinEditText.postDelayed({ + showInputMethod(binding.pinEditText) + }, 200) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt new file mode 100644 index 00000000..457d4b81 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -0,0 +1,92 @@ +package com.lagradost.cloudstream3.ui.account + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.CommonActivity +import com.lagradost.cloudstream3.CommonActivity.loadThemes +import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ActivityAccountSelectBinding +import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAccounts +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute + +class AccountSelectActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val accounts = getAccounts(this@AccountSelectActivity) + + // Don't show account selection if there is only + // one account that exists + if (accounts.count() <= 1) { + navigateToMainActivity() + return + } + + CommonActivity.init(this) + loadThemes(this) + + window.navigationBarColor = colorFromAttribute(R.attr.primaryBlackBackground) + + val binding = ActivityAccountSelectBinding.inflate(layoutInflater) + + setContentView(binding.root) + + val recyclerView: RecyclerView = binding.accountRecyclerView + + + val adapter = AccountAdapter(accounts) { selectedAccount -> + // Handle the selected account + onAccountSelected(selectedAccount) + } + recyclerView.adapter = adapter + + if (isTvSettings()) { + val spanSize = if (accounts.count() <= 6) { + accounts.count() + } else 6 + + recyclerView.layoutManager = GridLayoutManager(this, spanSize) + } + } + + private fun onAccountSelected(selectedAccount: DataStoreHelper.Account) { + if (selectedAccount.lockPin != null) { + // The selected account has a PIN set, prompt the user to enter the PIN + showPinInputDialog(this@AccountSelectActivity, selectedAccount.lockPin, false) { pin -> + if (pin == null) return@showPinInputDialog + // Pin is correct, proceed to main activity + setAccount(selectedAccount) + navigateToMainActivity() + } + } else { + // No PIN set for the selected account, proceed to main activity + setAccount(selectedAccount) + navigateToMainActivity() + } + } + + private fun setAccount(account: DataStoreHelper.Account) { + // Don't reload if it is the same account + if (DataStoreHelper.selectedKeyIndex == account.keyIndex) { + return + } + + DataStoreHelper.selectedKeyIndex = account.keyIndex + + MainActivity.bookmarksUpdatedEvent(true) + MainActivity.reloadHomeEvent(true) + } + + private fun navigateToMainActivity() { + val mainIntent = Intent(this, MainActivity::class.java) + startActivity(mainIntent) + finish() // Finish the account selection activity + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index b7e52b88..5f194f1f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -378,21 +378,25 @@ class HomeParentItemAdapterPreview( showApply = false, {}) { val newValue = WatchType.values()[it] - homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds( - null, - ContextCompat.getDrawable( - homePreviewBookmark.context, - newValue.iconRes - ), - null, - null - ) - homePreviewBookmark.setText(newValue.stringRes) - ResultViewModel2.updateWatchStatus( - item, - newValue - ) + ResultViewModel2().updateWatchStatus( + newValue, + fab.context, + item + ) { statusChanged: Boolean -> + if (!statusChanged) return@updateWatchStatus + + homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds( + null, + ContextCompat.getDrawable( + homePreviewBookmark.context, + newValue.iconRes + ), + null, + null + ) + homePreviewBookmark.setText(newValue.stringRes) + } } } } 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 ad75aa9d..f471fefd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -40,7 +40,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds @@ -102,11 +101,6 @@ class HomeViewModel : ViewModel() { loadStoredData() } - fun deleteBookmarks() { - deleteAllBookmarkedData() - loadStoredData() - } - var repo: APIRepository? = null private val _apiName = MutableLiveData() 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 c3986dca..864ca065 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 @@ -27,6 +27,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.viewpager2.widget.ViewPager2 import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder @@ -62,7 +63,7 @@ const val LIBRARY_FOLDER = "library_folder" enum class LibraryOpenerType(@StringRes val stringRes: Int) { - Default(R.string.default_subtitles), // TODO FIX AFTER MERGE + Default(R.string.action_default), Provider(R.string.none), Browser(R.string.browser), Search(R.string.search), @@ -137,6 +138,10 @@ class LibraryFragment : Fragment() { binding?.libraryRoot?.findViewById(R.id.search_src_text)?.apply { tag = "tv_no_focus_tag" + //Expand the Appbar when search bar is focused, fixing scroll up issue + setOnFocusChangeListener { _, _ -> + binding?.searchBar?.setExpanded(true) + } } // Set the color for the search exit icon to the correct theme text color @@ -342,6 +347,7 @@ class LibraryFragment : Fragment() { binding?.apply { viewpager.offscreenPageLimit = 2 viewpager.reduceDragSensitivity() + searchBar.setExpanded(true) } val startLoading = Runnable { @@ -441,6 +447,10 @@ class LibraryFragment : Fragment() { val distance = abs(position - currentItem) hideViewpager(distance) } + //Expand the appBar on tab focus + tab.view.setOnFocusChangeListener { view, b -> + binding?.searchBar?.setExpanded(true) + } }.attach() } } 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 76028487..6731eae2 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 @@ -1,14 +1,18 @@ package com.lagradost.cloudstream3.ui.library import android.os.Build +import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.doOnAttach import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnFlingListener +import com.google.android.material.appbar.AppBarLayout +import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.search.SearchClickCallback +import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount class ViewpagerAdapter( @@ -67,6 +71,17 @@ class ViewpagerAdapter( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> val diff = scrollY - oldScrollY + + //Expand the top Appbar based on scroll direction up/down, simulate phone behavior + if (SettingsFragment.isTvSettings()) { + binding.root.rootView.findViewById(R.id.search_bar) + .apply { + if (diff <= 0) + setExpanded(true) + else + setExpanded(false) + } + } if (diff == 0) return@setOnScrollChangeListener scrollCallback.invoke(diff > 0) @@ -80,8 +95,6 @@ class ViewpagerAdapter( } } } - - } } 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 ed9fd270..7bcce764 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -430,34 +430,36 @@ open class ResultFragmentPhone : FullScreenPlayer() { } }) resultSubscribe.setOnClickListener { - val isSubscribed = - viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener + viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleSubscriptionStatus - val message = if (isSubscribed) { - // Kinda icky to have this here, but it works. - SubscriptionWorkManager.enqueuePeriodicWork(context) - R.string.subscription_new - } else { - R.string.subscription_deleted + val message = if (newStatus) { + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } - - val name = (viewModel.page.value as? Resource.Success)?.value?.title - ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } resultFavorite.setOnClickListener { - val isFavorite = - viewModel.toggleFavoriteStatus() ?: return@setOnClickListener + viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleFavoriteStatus - val message = if (isFavorite) { - R.string.favorite_added - } else { - R.string.favorite_removed + val message = if (newStatus) { + R.string.favorite_added + } else { + R.string.favorite_removed + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } - - val name = (viewModel.page.value as? Resource.Success)?.value?.title - ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } mediaRouteButton.apply { val chromecastSupport = api?.hasChromecastSupport == true @@ -960,7 +962,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it]) + viewModel.updateWatchStatus(WatchType.values()[it], context) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index feb8ca04..396ec863 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -28,6 +28,7 @@ import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable +import com.lagradost.cloudstream3.services.SubscriptionWorkManager import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator @@ -37,6 +38,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isRtl @@ -287,7 +289,8 @@ class ResultFragmentTv : Fragment() { resultResumeSeries, resultPlayTrailer, resultBookmarkButton, - resultFavoriteButton + resultFavoriteButton, + resultSubscribeButton ) for (requestView in views) { if (!requestView.isVisible) continue @@ -429,6 +432,7 @@ class ResultFragmentTv : Fragment() { binding?.resultEpisodesShow, binding?.resultBookmarkButton, binding?.resultFavoriteButton, + binding?.resultSubscribeButton, ).firstOrNull { it?.isVisible == true } @@ -531,7 +535,7 @@ class ResultFragmentTv : Fragment() { view.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it]) + viewModel.updateWatchStatus(WatchType.values()[it], context) } } } @@ -557,17 +561,58 @@ class ResultFragmentTv : Fragment() { setIconResource(drawable) setText(text) setOnClickListener { - val isFavorite = viewModel.toggleFavoriteStatus() ?: return@setOnClickListener + viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleFavoriteStatus - val message = if (isFavorite) { - R.string.favorite_added - } else { - R.string.favorite_removed + val message = if (newStatus) { + R.string.favorite_added + } else { + R.string.favorite_removed + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } + } + } + } - val name = (viewModel.page.value as? Resource.Success)?.value?.title - ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + observeNullable(viewModel.subscribeStatus) { isSubscribed -> + binding?.resultSubscribeButton?.apply { + isVisible = isSubscribed != null && context.isEmulatorSettings() + if (isSubscribed == null) return@observeNullable + + val drawable = if (isSubscribed) { + R.drawable.ic_baseline_notifications_active_24 + } else { + R.drawable.baseline_notifications_none_24 + } + + val text = if (isSubscribed) { + R.string.action_unsubscribe + } else { + R.string.action_subscribe + } + + setIconResource(drawable) + setText(text) + setOnClickListener { + viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleSubscriptionStatus + + val message = if (newStatus) { + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + } } } } 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 38374b11..d744fac5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -7,6 +7,8 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.widget.Toast +import androidx.annotation.MainThread +import androidx.appcompat.app.AlertDialog import androidx.core.content.FileProvider import androidx.core.net.toUri import androidx.lifecycle.LiveData @@ -31,6 +33,7 @@ import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.providers.Kitsu +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO @@ -45,22 +48,37 @@ import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe import com.lagradost.cloudstream3.utils.Coroutines.main +import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions +import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState +import com.lagradost.cloudstream3.utils.DataStoreHelper.getSubscribedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData +import com.lagradost.cloudstream3.utils.DataStoreHelper.removeSubscribedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason +import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState +import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState +import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.UIHelper.navigate import kotlinx.coroutines.* import java.io.File @@ -113,6 +131,18 @@ data class ResultData( val plotHeaderText: UiText, ) +data class CheckDuplicateData( + val name: String, + val year: Int?, + val syncData: Map? +) + +enum class LibraryListType { + BOOKMARKS, + FAVORITES, + SUBSCRIPTIONS +} + fun txt(status: DubStatus?): UiText? { return txt( when (status) { @@ -441,33 +471,6 @@ class ResultViewModel2 : ViewModel() { return this?.firstOrNull { it.season == season } } - fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) { - val currentId = currentResponse.getId() - - val currentWatchType = getResultWatchState(currentId) - - DataStoreHelper.setResultWatchState(currentId, status.internalId) - val current = DataStoreHelper.getBookmarkedData(currentId) - val currentTime = System.currentTimeMillis() - DataStoreHelper.setBookmarkedData( - currentId, - DataStoreHelper.BookmarkedData( - currentId, - current?.bookmarkedTime ?: currentTime, - currentTime, - currentResponse.name, - currentResponse.url, - currentResponse.apiName, - currentResponse.type, - currentResponse.posterUrl, - currentResponse.year - ) - ) - if (currentWatchType != status) { - MainActivity.bookmarksUpdatedEvent(true) - } - } - private fun filterName(name: String?): String? { if (name == null) return null Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let { @@ -822,9 +825,77 @@ class ResultViewModel2 : ViewModel() { val selectPopup: LiveData = _selectPopup - fun updateWatchStatus(status: WatchType) { - updateWatchStatus(currentResponse ?: return, status) - _watchStatus.postValue(status) + fun updateWatchStatus( + status: WatchType, + context: Context?, + loadResponse: LoadResponse? = null, + statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null + ) { + val response = loadResponse ?: currentResponse ?: return + + val currentId = response.getId() + + val currentStatus = getResultWatchState(currentId) + + // If the current status is "NONE" and the new status is not "NONE", + // fetch the bookmarked data to check for duplicates, otherwise set this + // to an empty list, so that we don't show the duplicate warning dialog, + // but we still want to update the current bookmark and refresh the data anyway. + val bookmarkedData = if (currentStatus == WatchType.NONE && status != WatchType.NONE) { + getAllBookmarkedData() + } else emptyList() + + checkAndWarnDuplicates( + context, + LibraryListType.BOOKMARKS, + CheckDuplicateData( + name = response.name, + year = response.year, + syncData = response.syncData, + ), + bookmarkedData + ) { shouldContinue: Boolean, duplicateIds: List -> + if (!shouldContinue) return@checkAndWarnDuplicates + + if (duplicateIds.isNotEmpty()) { + duplicateIds.forEach { duplicateId -> + deleteBookmarkedData(duplicateId) + } + } + + setResultWatchState(currentId, status.internalId) + + // We don't need to store if WatchType.NONE. + // The key is removed in setResultWatchState, we don't want to + // re-add it again here if it was just removed. + if (status != WatchType.NONE) { + val current = getBookmarkedData(currentId) + + setBookmarkedData( + currentId, + DataStoreHelper.BookmarkedData( + current?.bookmarkedTime ?: unixTimeMS, + currentId, + unixTimeMS, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl, + response.year, + response.syncData + ) + ) + } + + if (currentStatus != status) { + MainActivity.bookmarksUpdatedEvent(true) + } + + _watchStatus.postValue(status) + + statusChangedCallback?.invoke(true) + } } private fun startChromecast( @@ -839,73 +910,255 @@ class ResultViewModel2 : ViewModel() { } /** - * @return true if the new status is Subscribed, false if not. Null if not possible to subscribe. - **/ - fun toggleSubscriptionStatus(): Boolean? { - val isSubscribed = _subscribeStatus.value ?: return null - val response = currentResponse ?: return null - if (response !is EpisodeResponse) return null + * Toggles the subscription status of an item. + * + * @param context The context to use for operations. + * @param statusChangedCallback A callback that is invoked when the subscription status changes. + * It provides the new subscription status (true if subscribed, false if unsubscribed, null if action was canceled). + */ + fun toggleSubscriptionStatus( + context: Context?, + statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null + ) { + val isSubscribed = _subscribeStatus.value ?: return + val response = currentResponse ?: return + if (response !is EpisodeResponse) return val currentId = response.getId() if (isSubscribed) { - DataStoreHelper.removeSubscribedData(currentId) + removeSubscribedData(currentId) + statusChangedCallback?.invoke(false) + _subscribeStatus.postValue(false) } else { - val current = DataStoreHelper.getSubscribedData(currentId) + checkAndWarnDuplicates( + context, + LibraryListType.SUBSCRIPTIONS, + CheckDuplicateData( + name = response.name, + year = response.year, + syncData = response.syncData, + ), + getAllSubscriptions(), + ) { shouldContinue: Boolean, duplicateIds: List -> + if (!shouldContinue) { + statusChangedCallback?.invoke(null) + return@checkAndWarnDuplicates + } - DataStoreHelper.setSubscribedData( - currentId, - DataStoreHelper.SubscribedData( + if (duplicateIds.isNotEmpty()) { + duplicateIds.forEach { duplicateId -> + removeSubscribedData(duplicateId) + } + } + + val current = getSubscribedData(currentId) + + setSubscribedData( currentId, - current?.bookmarkedTime ?: unixTimeMS, - unixTimeMS, - response.getLatestEpisodes(), - response.name, - response.url, - response.apiName, - response.type, - response.posterUrl, - response.year + DataStoreHelper.SubscribedData( + current?.subscribedTime ?: unixTimeMS, + response.getLatestEpisodes(), + currentId, + unixTimeMS, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl, + response.year, + response.syncData + ) ) - ) - } - _subscribeStatus.postValue(!isSubscribed) - return !isSubscribed + _subscribeStatus.postValue(true) + + statusChangedCallback?.invoke(true) + } + } } /** - * @return true if added to favorites, false if not. Null if not possible to favorite. - **/ - fun toggleFavoriteStatus(): Boolean? { - val isFavorite = _favoriteStatus.value ?: return null - val response = currentResponse ?: return null + * Toggles the favorite status of an item. + * + * @param context The context to use. + * @param statusChangedCallback A callback that is invoked when the favorite status changes. + * It provides the new favorite status (true if added to favorites, false if removed, null if action was canceled). + */ + fun toggleFavoriteStatus( + context: Context?, + statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null + ) { + val isFavorite = _favoriteStatus.value ?: return + val response = currentResponse ?: return val currentId = response.getId() if (isFavorite) { removeFavoritesData(currentId) + statusChangedCallback?.invoke(false) + _favoriteStatus.postValue(false) } else { - val current = getFavoritesData(currentId) + checkAndWarnDuplicates( + context, + LibraryListType.FAVORITES, + CheckDuplicateData( + name = response.name, + year = response.year, + syncData = response.syncData, + ), + getAllFavorites(), + ) { shouldContinue: Boolean, duplicateIds: List -> + if (!shouldContinue) { + statusChangedCallback?.invoke(null) + return@checkAndWarnDuplicates + } - setFavoritesData( - currentId, - DataStoreHelper.FavoritesData( + if (duplicateIds.isNotEmpty()) { + duplicateIds.forEach { duplicateId -> + removeFavoritesData(duplicateId) + } + } + + val current = getFavoritesData(currentId) + + setFavoritesData( currentId, - current?.favoritesTime ?: unixTimeMS, - unixTimeMS, - response.name, - response.url, - response.apiName, - response.type, - response.posterUrl, - response.year + DataStoreHelper.FavoritesData( + current?.favoritesTime ?: unixTimeMS, + currentId, + unixTimeMS, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl, + response.year, + response.syncData + ) ) - ) + + _favoriteStatus.postValue(true) + + statusChangedCallback?.invoke(true) + } + } + } + + @MainThread + private fun checkAndWarnDuplicates( + context: Context?, + listType: LibraryListType, + checkDuplicateData: CheckDuplicateData, + data: List, + checkDuplicatesCallback: (shouldContinue: Boolean, duplicateIds: List) -> Unit + ) { + val whitespaceRegex = "\\s+".toRegex() + fun normalizeString(input: String): String { + /** + * Trim the input string and replace consecutive spaces with a single space. + * This covers some edge-cases where the title does not match exactly across providers, + * and one provider has the title with an extra whitespace. This is minor enough that + * it should still match in this case. + */ + return input.trim().replace(whitespaceRegex, " ") } - _favoriteStatus.postValue(!isFavorite) - return !isFavorite + val syncData = checkDuplicateData.syncData + + val imdbId = getImdbIdFromSyncData(syncData) + val tmdbId = getTMDbIdFromSyncData(syncData) + val malId = syncData?.get(AccountManager.malApi.idPrefix) + val aniListId = syncData?.get(AccountManager.aniListApi.idPrefix) + val normalizedName = normalizeString(checkDuplicateData.name) + val year = checkDuplicateData.year + + val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> + val librarySyncData = it.syncData + + val checks = listOf( + { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, + { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, + { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, + { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, + { normalizedName == normalizeString(it.name) && year == it.year } + ) + + checks.any { it() } + } + + if (duplicateEntries.isEmpty() || context == null) { + checkDuplicatesCallback.invoke(true, emptyList()) + return + } + + val replaceMessage = if (duplicateEntries.size > 1) { + R.string.duplicate_replace_all + } else R.string.duplicate_replace + + val message = if (duplicateEntries.size == 1) { + val list = when (listType) { + LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes + LibraryListType.FAVORITES -> R.string.favorites_list_name + LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name + } + + context.getString(R.string.duplicate_message_single, + "${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}" + ) + } else { + val bulletPoints = duplicateEntries.joinToString("\n") { + val list = when (listType) { + LibraryListType.BOOKMARKS -> getResultWatchState(it.id ?: 0).stringRes + LibraryListType.FAVORITES -> R.string.favorites_list_name + LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name + } + + "• ${it.apiName}: ${normalizeString(it.name)} (${context.getString(list)})" + } + + context.getString(R.string.duplicate_message_multiple, bulletPoints) + } + + val builder: AlertDialog.Builder = AlertDialog.Builder(context) + + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + checkDuplicatesCallback.invoke(true, emptyList()) + } + DialogInterface.BUTTON_NEGATIVE -> { + checkDuplicatesCallback.invoke(false, emptyList()) + } + DialogInterface.BUTTON_NEUTRAL -> { + checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id }) + } + } + } + + builder.setTitle(R.string.duplicate_title) + .setMessage(message) + .setPositiveButton(R.string.duplicate_add, dialogClickListener) + .setNegativeButton(R.string.duplicate_cancel, dialogClickListener) + .setNeutralButton(replaceMessage, dialogClickListener) + .show().setDefaultFocus() + } + + private fun getImdbIdFromSyncData(syncData: Map?): String? { + return normalSafeApiCall { + SimklApi.readIdFromString( + syncData?.get(AccountManager.simklApi.idPrefix) + )[SimklApi.Companion.SyncServices.Imdb] + } + } + + private fun getTMDbIdFromSyncData(syncData: Map?): String? { + return normalSafeApiCall { + SimklApi.readIdFromString( + syncData?.get(AccountManager.simklApi.idPrefix) + )[SimklApi.Companion.SyncServices.Tmdb] + } } private fun startChromecast( @@ -1259,7 +1512,7 @@ class ResultViewModel2 : ViewModel() { // Do not add mark as watched on movies if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) { val isWatched = - DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched + getVideoWatchState(click.data.id) == VideoWatchState.Watched val watchedText = if (isWatched) R.string.action_remove_from_watched else R.string.action_mark_as_watched @@ -1508,12 +1761,12 @@ class ResultViewModel2 : ViewModel() { ACTION_MARK_AS_WATCHED -> { val isWatched = - DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched + getVideoWatchState(click.data.id) == VideoWatchState.Watched if (isWatched) { - DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.None) + setVideoWatchState(click.data.id, VideoWatchState.None) } else { - DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.Watched) + setVideoWatchState(click.data.id, VideoWatchState.Watched) } // Kinda dirty to reload all episodes :( @@ -1722,7 +1975,7 @@ class ResultViewModel2 : ViewModel() { list.subList(start, end).map { val posDur = getViewPos(it.id) val watchState = - DataStoreHelper.getVideoWatchState(it.id) ?: VideoWatchState.None + getVideoWatchState(it.id) ?: VideoWatchState.None it.copy( position = posDur?.position ?: 0, duration = posDur?.duration ?: 0, @@ -1783,8 +2036,8 @@ class ResultViewModel2 : ViewModel() { private fun postSubscription(loadResponse: LoadResponse) { if (loadResponse.isEpisodeBased()) { val id = loadResponse.getId() - val data = DataStoreHelper.getSubscribedData(id) - DataStoreHelper.updateSubscribedData(id, data, loadResponse as? EpisodeResponse) + val data = getSubscribedData(id) + updateSubscribedData(id, data, loadResponse as? EpisodeResponse) val isSubscribed = data != null _subscribeStatus.postValue(isSubscribed) } @@ -2174,13 +2427,13 @@ class ResultViewModel2 : ViewModel() { postResume() } - fun postResume() { + private fun postResume() { _resumeWatching.postValue(resume()) } private fun resume(): ResumeWatchingStatus? { val correctId = currentId ?: return null - val resume = DataStoreHelper.getLastWatched(correctId) + val resume = getLastWatched(correctId) val resumeParentId = resume?.parentId if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched val resumeId = resume.episodeId ?: return null// invalid episode id diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index b3225d5c..aa5a3182 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 @@ -30,6 +30,7 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -248,6 +249,7 @@ class SettingsAccount : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_account) setPaddingBottom() + setToolBarScrollFlags() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: 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 a4d19eba..cb48b086 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 @@ -12,10 +12,12 @@ 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 import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding @@ -54,7 +56,15 @@ class SettingsFragment : Fragment() { listView?.setPadding(0, 0, 0, 100.toPx) } } + fun PreferenceFragmentCompat.setToolBarScrollFlags() { + if (isTvSettings()) { + val settingsAppbar = view?.findViewById(R.id.settings_toolbar) + settingsAppbar?.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } + } + } fun Fragment?.setUpToolbar(title: String) { if (this == null) return val settingsToolbar = view?.findViewById(R.id.settings_toolbar) ?: return 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 17efd276..224ca74a 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,6 +28,7 @@ import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke 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.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -115,6 +116,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_general) setPaddingBottom() + setToolBarScrollFlags() } data class CustomSite( @@ -192,7 +194,6 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener 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/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index e10a5a1a..3d0bcb1f 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,6 +10,7 @@ import com.lagradost.cloudstream3.mvvm.logError 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.setPaddingBottom +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment @@ -23,6 +24,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_player) setPaddingBottom() + setToolBarScrollFlags() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { hideKeyboard() 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 7e57fc5b..7dc73a46 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 @@ -13,6 +13,7 @@ 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.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog @@ -25,6 +26,7 @@ class SettingsProviders : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_providers) setPaddingBottom() + setToolBarScrollFlags() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 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 e2fd24ca..8c720313 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 @@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.search.SearchResultBuilder 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.ui.settings.SettingsFragment.Companion.updateTv import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog @@ -23,6 +24,7 @@ class SettingsUI : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_ui) setPaddingBottom() + setToolBarScrollFlags() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 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 2f796801..9f72c1d5 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 @@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.services.BackupWorkManager import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt @@ -42,6 +43,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_updates) setPaddingBottom() + setToolBarScrollFlags() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 48917889..1be966b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -583,7 +583,7 @@ object AppUtils { //private val viewModel: ResultViewModel by activityViewModels() private fun getResultsId(): Int { - return if (isTrueTvSettings()) { + return if (isTvSettings()) { R.id.global_to_navigation_results_tv } else { R.id.global_to_navigation_results_phone diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 4b4157d6..e687bcfb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -6,6 +6,7 @@ import android.text.Editable import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.material.bottomsheet.BottomSheetDialog @@ -26,8 +27,8 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter +import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog import com.lagradost.cloudstream3.ui.library.ListSorting -import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.ui.result.setImage @@ -136,6 +137,8 @@ object DataStoreHelper { val customImage: String? = null, @JsonProperty("defaultImageIndex") val defaultImageIndex: Int, + @JsonProperty("lockPin") + val lockPin: String? = null, ) { val image: UiImage get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable( @@ -230,36 +233,86 @@ object DataStoreHelper { binding.profilePic.setImage(account.image) binding.profilePic.setOnClickListener { - // rolls the image forwards once + // Roll the image forwards once currentEditAccount = currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size) binding.profilePic.setImage(currentEditAccount.image) } binding.applyBtt.setOnClickListener { - val currentAccounts = accounts.toMutableList() - - val overrideIndex = - currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex } - - // if an account is found that has the same keyIndex then override that one, if not then append it - if (overrideIndex != -1) { - currentAccounts[overrideIndex] = currentEditAccount + if (currentEditAccount.lockPin != null) { + // Ask for the current PIN + showPinInputDialog(context, currentEditAccount.lockPin, false) { pin -> + if (pin == null) return@showPinInputDialog + // PIN is correct, proceed to update the account + performAccountUpdate(currentEditAccount) + dialog.dismissSafe() + } } else { - currentAccounts.add(currentEditAccount) + // No lock PIN set, proceed to update the account + performAccountUpdate(currentEditAccount) + dialog.dismissSafe() } - - // Save the current homepage for new accounts - val currentHomePage = DataStoreHelper.currentHomePage - - // set the new default account as well as add the key for the new account - setAccount(currentEditAccount, false) - DataStoreHelper.currentHomePage = currentHomePage - - accounts = currentAccounts.toTypedArray() - - dialog.dismissSafe() } + + // Handle setting or changing the PIN + + if (currentEditAccount.keyIndex == getDefaultAccount(context).keyIndex) { + binding.lockProfileCheckbox.isVisible = false + if (currentEditAccount.lockPin != null) { + currentEditAccount = currentEditAccount.copy(lockPin = null) + } + } + + var canSetPin = true + + binding.lockProfileCheckbox.isChecked = currentEditAccount.lockPin != null + + binding.lockProfileCheckbox.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + if (canSetPin) { + showPinInputDialog(context, null, true) { pin -> + if (pin == null) { + binding.lockProfileCheckbox.isChecked = false + return@showPinInputDialog + } + + currentEditAccount = currentEditAccount.copy(lockPin = pin) + } + } + } else { + if (currentEditAccount.lockPin != null) { + // Ask for the current PIN + showPinInputDialog(context, currentEditAccount.lockPin, true) { pin -> + if (pin == null || pin != currentEditAccount.lockPin) { + canSetPin = false + binding.lockProfileCheckbox.isChecked = true + } else { + currentEditAccount = currentEditAccount.copy(lockPin = null) + } + } + } + } + } + + canSetPin = true + } + + private fun performAccountUpdate(account: Account) { + val currentAccounts = accounts.toMutableList() + + val overrideIndex = currentAccounts.indexOfFirst { it.keyIndex == account.keyIndex } + + if (overrideIndex != -1) { + currentAccounts[overrideIndex] = account + } else { + currentAccounts.add(account) + } + + val currentHomePage = this.currentHomePage + setAccount(account, false) + this.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() } private fun getDefaultAccount(context: Context): Account { @@ -272,10 +325,18 @@ object DataStoreHelper { } } + fun getAccounts(context: Context): List { + return accounts.toMutableList().apply { + val item = getDefaultAccount(context) + remove(item) + add(0, item) + } + } + fun showWhoIsWatching(context: Context) { - val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate( - LayoutInflater.from(context) - ) + val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(LayoutInflater.from(context)) + val builder = BottomSheetDialog(context) + builder.setContentView(binding.root) val showAccount = accounts.toMutableList().apply { val item = getDefaultAccount(context) @@ -283,22 +344,25 @@ object DataStoreHelper { add(0, item) } - val builder = - BottomSheetDialog(context) - builder.setContentView(binding.root) val accountName = context.getString(R.string.account) - binding.profilesRecyclerview.setLinearListLayout( - isHorizontal = true, - nextUp = FOCUS_SELF, - nextDown = FOCUS_SELF, - nextLeft = FOCUS_SELF, - nextRight = FOCUS_SELF - ) + binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account, true) - builder.dismissSafe() + // Check if the selected account has a lock PIN set + if (account.lockPin != null) { + // Prompt for the lock pin + showPinInputDialog(context, account.lockPin, false) { pin -> + if (pin == null) return@showPinInputDialog + // Pin is correct, unlock the profile + setAccount(account, true) + builder.dismissSafe() + } + } else { + // No lock PIN set, directly set the account + setAccount(account, true) + builder.dismissSafe() + } }, addAccountCallback = { val currentAccounts = accounts @@ -334,7 +398,6 @@ object DataStoreHelper { builder.show() } - data class PosDur( @JsonProperty("position") val position: Long, @JsonProperty("duration") val duration: Long @@ -352,20 +415,35 @@ object DataStoreHelper { /** * Used to display notifications on new episodes and posters in library. **/ - data class SubscribedData( + abstract class LibrarySearchResponse( @JsonProperty("id") override var id: Int?, - @JsonProperty("subscribedTime") val bookmarkedTime: Long, - @JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, - @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, + @JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long, @JsonProperty("name") override val name: String, @JsonProperty("url") override val url: String, @JsonProperty("apiName") override val apiName: String, - @JsonProperty("type") override var type: TvType? = null, + @JsonProperty("type") override var type: TvType?, @JsonProperty("posterUrl") override var posterUrl: String?, - @JsonProperty("year") val year: Int?, - @JsonProperty("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - ) : SearchResponse { + @JsonProperty("year") open val year: Int?, + @JsonProperty("syncData") open val syncData: Map?, + @JsonProperty("quality") override var quality: SearchQuality?, + @JsonProperty("posterHeaders") override var posterHeaders: Map? + ) : SearchResponse + + data class SubscribedData( + @JsonProperty("subscribedTime") val subscribedTime: Long, + @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) { fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -381,18 +459,19 @@ object DataStoreHelper { } data class BookmarkedData( - @JsonProperty("id") override var id: Int?, @JsonProperty("bookmarkedTime") val bookmarkedTime: Long, - @JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, - @JsonProperty("name") override val name: String, - @JsonProperty("url") override val url: String, - @JsonProperty("apiName") override val apiName: String, - @JsonProperty("type") override var type: TvType? = null, - @JsonProperty("posterUrl") override var posterUrl: String?, - @JsonProperty("year") val year: Int?, - @JsonProperty("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - ) : SearchResponse { + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) { fun toLibraryItem(id: String): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( name, @@ -408,18 +487,19 @@ object DataStoreHelper { } data class FavoritesData( - @JsonProperty("id") override var id: Int?, @JsonProperty("favoritesTime") val favoritesTime: Long, - @JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, - @JsonProperty("name") override val name: String, - @JsonProperty("url") override val url: String, - @JsonProperty("apiName") override val apiName: String, - @JsonProperty("type") override var type: TvType? = null, - @JsonProperty("posterUrl") override var posterUrl: String?, - @JsonProperty("year") val year: Int?, - @JsonProperty("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - ) : SearchResponse { + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) { fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -466,15 +546,9 @@ object DataStoreHelper { removeKeys(folder) } - fun deleteAllBookmarkedData() { - val folder1 = "$currentAccount/$RESULT_WATCH_STATE" - val folder2 = "$currentAccount/$RESULT_WATCH_STATE_DATA" - removeKeys(folder1) - removeKeys(folder2) - } - fun deleteBookmarkedData(id: Int?) { if (id == null) return + AccountManager.localListApi.requireLibraryRefresh = true removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) } @@ -572,6 +646,12 @@ object DataStoreHelper { return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) } + fun getAllBookmarkedData(): List { + return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull { + getKey(it) + } ?: emptyList() + } + fun getAllSubscriptions(): List { return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull { getKey(it) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 5d54ffe5..f34e7238 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -7,10 +7,7 @@ import android.view.LayoutInflater import android.view.View import android.widget.AbsListView import android.widget.ArrayAdapter -import android.widget.EditText -import android.widget.ImageView import android.widget.LinearLayout -import android.widget.ListView import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone @@ -20,8 +17,10 @@ import androidx.core.view.marginRight import androidx.core.view.marginTop import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding +import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding +import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding +import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes @@ -56,14 +55,14 @@ object SingleSelectionHelper { if (this == null) return if (isTvSettings()) { - val builder = - AlertDialog.Builder(this, R.style.AlertDialogCustom) - .setView(R.layout.options_popup_tv) + val binding = OptionsPopupTvBinding.inflate(layoutInflater) + val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom) + .setView(binding.root) + .create() - val dialog = builder.create() dialog.show() - dialog.findViewById(R.id.listview1)?.let { listView -> + binding.listview1.let { listView -> listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE listView.adapter = ArrayAdapter(this, R.layout.sort_bottom_single_choice_color).apply { @@ -76,7 +75,7 @@ object SingleSelectionHelper { } } - dialog.findViewById(R.id.imageView)?.apply { + binding.imageView.apply { isGone = poster.isNullOrEmpty() setImage(poster) } @@ -107,12 +106,12 @@ object SingleSelectionHelper { if (this == null) return val realShowApply = showApply || isMultiSelect - val listView = binding.listview1//.findViewById(R.id.listview1)!! - val textView = binding.text1//.findViewById(R.id.text1)!! - val applyButton = binding.applyBtt//.findViewById(R.id.apply_btt) - val cancelButton = binding.cancelBtt//findViewById(R.id.cancel_btt) + val listView = binding.listview1 + val textView = binding.text1 + val applyButton = binding.applyBtt + val cancelButton = binding.cancelBtt val applyHolder = - binding.applyBttHolder//.findViewById(R.id.apply_btt_holder) + binding.applyBttHolder applyHolder.isVisible = realShowApply if (!realShowApply) { @@ -175,8 +174,8 @@ object SingleSelectionHelper { } } - private fun Activity?.showInputDialog( + binding: BottomInputDialogBinding, dialog: Dialog, value: String, name: String, @@ -186,11 +185,11 @@ object SingleSelectionHelper { ) { if (this == null) return - val inputView = dialog.findViewById(R.id.nginx_text_input)!! - val textView = dialog.findViewById(R.id.text1)!! - val applyButton = dialog.findViewById(R.id.apply_btt)!! - val cancelButton = dialog.findViewById(R.id.cancel_btt)!! - val applyHolder = dialog.findViewById(R.id.apply_btt_holder)!! + val inputView = binding.nginxTextInput + val textView = binding.text1 + val applyButton = binding.applyBtt + val cancelButton = binding.cancelBtt + val applyHolder = binding.applyBttHolder applyHolder.isVisible = true textView.text = name @@ -352,11 +351,17 @@ object SingleSelectionHelper { dismissCallback: () -> Unit, callback: (String) -> Unit, ) { - val builder = BottomSheetDialog(this) // probably the stuff at the bottom - builder.setContentView(R.layout.bottom_input_dialog) // input layout + val builder = BottomSheetDialog(this) + + val binding: BottomInputDialogBinding = BottomInputDialogBinding.inflate( + LayoutInflater.from(this) + ) + + builder.setContentView(binding.root) builder.show() showInputDialog( + binding, builder, value, name, diff --git a/app/src/main/res/layout/account_list_item.xml b/app/src/main/res/layout/account_list_item.xml new file mode 100644 index 00000000..3331b85b --- /dev/null +++ b/app/src/main/res/layout/account_list_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account_select.xml b/app/src/main/res/layout/activity_account_select.xml new file mode 100644 index 00000000..d5870f24 --- /dev/null +++ b/app/src/main/res/layout/activity_account_select.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home_head.xml b/app/src/main/res/layout/fragment_home_head.xml index 90386ccf..0edaf230 100644 --- a/app/src/main/res/layout/fragment_home_head.xml +++ b/app/src/main/res/layout/fragment_home_head.xml @@ -92,15 +92,7 @@ android:layout_height="100dp" android:layout_gravity="bottom" android:gravity="center" - android:orientation="vertical"> - - + android:orientation="horizontal"> - + diff --git a/app/src/main/res/layout/fragment_library_tv.xml b/app/src/main/res/layout/fragment_library_tv.xml index 22b9feb1..6d2198e9 100644 --- a/app/src/main/res/layout/fragment_library_tv.xml +++ b/app/src/main/res/layout/fragment_library_tv.xml @@ -27,7 +27,8 @@ android:id="@+id/search_status_bar_padding" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal"> + android:orientation="horizontal" + app:layout_scrollFlags="scroll|enterAlways"> + - + - + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/who_is_watching_account.xml b/app/src/main/res/layout/who_is_watching_account.xml index 4970d004..8152ed27 100644 --- a/app/src/main/res/layout/who_is_watching_account.xml +++ b/app/src/main/res/layout/who_is_watching_account.xml @@ -35,6 +35,15 @@ android:background="@drawable/outline_card" android:visibility="gone" /> + + + + ማውረድ ቀጥል የጽሑፍ ቀለም የተጠናቀቀ - ምንም የፊልም ማስታወቂያ አጫውት የቀጥታ ስርጭት አጫውት ፋይል አጫውት እንደገና በማየት ላይ - ሰርዝ ወደ ኋላ መመለሻ መረጃ ያስቀምጡ diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8805ec5d..56e19280 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -12,8 +12,8 @@ سرعة (%.2fx) تقييم: %.1f - !تم العثور على تحديث جديد -\n%s -> %s + يوجد تحديث جديد! +\n%1$s -> %2$s %d دقيقة CloudStream تشغيل بواسطة CloudStream @@ -36,7 +36,6 @@ مكتمل مهمل أخطط لمشاهدته - لا شيء إعادة المشاهدة مشاهدة الفيلم تشغيل بث حي @@ -74,7 +73,6 @@ ازالة إعداد حالة المشاهدة تطبيق - إلغاء نسخ إغلاق مسح @@ -180,6 +178,7 @@ لم يتم العثور على أي حلقات حذف الملف حذف + إلغاء إيقاف مؤقت إستئناف -٣٠ @@ -198,7 +197,7 @@ القصة في قائمة الانتظار الترجمة ليست موجودة - الإفتراضي + الإفتراضي فارغ مستخدم التطبيق @@ -320,7 +319,7 @@ Kitsu Trakt --> - %s %s + %1$s %2$s حساب تسجيل الخروج تسجيل الدخول @@ -419,8 +418,8 @@ تم إزالة الإضافة تعذر التحميل %s 18+ - بدأ تنزيل %d %s … - تم التنزيل %d %s + بدأ تنزيل %1$d %2$s… + تم تنزيل %1$d %2$s جميع %s محملة بالفعل تحميل مكثف إضافة @@ -462,11 +461,11 @@ السجل عرض زر تخطي المقدمة/الخاتمة طاقم العمل: %s - %d يوم %d ساعة %d دقيقة - %d ساعة %d دقيقة + %1$d يوم %2$d ساعة %3$d دقيقة + %1$d ساعة %2$d دقيقة الفيلير فتح(تشغيل) - %s %d%s + %1$s %2$d%3$s المكونات الإضافية المحدثة %d VLC MPV @@ -483,7 +482,7 @@ علّمه كفيديو تمت مشاهدته نعم - %s الحلقة %d + %1$s الحلقة %2$d سيتم إصدار الحلقة %d في تعذر تثبيت الإصدار الجديد من التطبيق تثبيت الإضافة أولا @@ -494,8 +493,8 @@ ‌تنزيل تحديث التطبيق… ‏تثبيت تحديث التطبيق… %d دقيقة - %d-%d - %d %s + %1$d-%2$d + %1$d %2$s هل أنت متأكد أنك تريد الخروج؟ قم بتثبيت جميع المكونات الإضافية التي لم يتم تثبيتها بعد تلقائيا من المستودعات المضافة. مثبت الحزم @@ -581,9 +580,34 @@ تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s حدد الوضع لتصفية تنزيل المكونات الإضافية تعطيل - @string/default_subtitles لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) لقد صوتت بالفعل معدل النسخ الإحتياطي + تمت إزالة %s من المفضلة + المفضلة + تمت إضافة %s إلى المفضلة + احتمال وجود تكرارات في مكتبتك. +\n +\n%s +\n +\nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟ + احتمال أن يكون موجود بالفعل + قفل الحساب + اضافة الى المفضلة + تبديل الكل + رقم PIN غير صحيح. برجاء المحاولة مرة اخرى. + إلغاء الاشتراك + رقم ال PIN يجب ان يكون 4 ارقام + استبدال + اضافة + إشترك + إزالة من المفضلة + اختار حساب + من الظاهر أن \"%1$s\" موجود بالفعل في مكتبتك. +\n +\nهل تريد الاضافة على أي حال مستبدلاً القديم أو إلغاء العملية؟ + ادخال ال PIN + PIN + أدخل ال PIN الحالي diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index a1042b7e..11dbddc0 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -34,7 +34,6 @@ الأنواع توقف التنزيل خطط للمشاهدة - لا يوجد إعادة المشاهدة !تم العثور على تحديث جديد \n%s->%s @@ -125,6 +124,7 @@ موسم تم نسخ الرابط إلى الحافظة مسح + الغي وقف جارٍ تنزيل تحديث التطبيق… إعادة التعيين إلى القيمة العادية @@ -208,7 +208,6 @@ استئناف تحميل معلومات وقفة التحميل - الغي احفظ إعدادات الترجمة لون الخط diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index e9ae65a1..e8c5d0f5 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -41,7 +41,6 @@ Завършено Изпуснат План за гледане - Нито един Повторно гледане Пускане на филм Възпроизвеждане на живо @@ -78,7 +77,6 @@ Премахване Задайте статус на гледане Приложи - Отказ Копирай Затвори Изчисти @@ -187,6 +185,7 @@ Няма намерени епизоди Изтрий файла Изтрий + Отказ Пауза Продължи -30 @@ -205,7 +204,7 @@ Синопсис На опашката Без субтитри - По подразбиране + По подразбиране Безплатно Използвано Приложения diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index c55c8943..b0359c1c 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -43,7 +43,6 @@ শেষ বাদ দেখার ইচ্ছায় - কোন কিছুই না পুনরায় দেখা হচ্ছে টরেন্ট স্ট্রিম করুন উৎসসমূহ @@ -72,7 +71,6 @@ বাদ দিন বুকমার্ক করুন প্রয়োগ করুন - বাদ দিন কপি করুন বন্ধ করুন মুছুন diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 7116a167..1c26c236 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -44,7 +44,6 @@ Completado Deixado Planejando assistir - Nenhum Reassistindo Assistir Filme Transmitir Torrent @@ -81,7 +80,6 @@ Remover Selecionar marcador Aplicar - Cancelar Copiar Fechar Limpar @@ -185,6 +183,7 @@ Nenhum Episódio encontrado Apagar Arquivo Deletar + Cancelar Pausar Retomar -30 @@ -203,7 +202,7 @@ Sinopse Na fila Sem Legendas - Padrão + Padrão Livre Usado App @@ -569,7 +568,6 @@ Wi-Fi Lista de videos da web A interface de usuário não foi gerada corretamente. Isto se trata de um bug importante e deve ser reportado imediatamente %s - Legendas padrão da conta Características da interface de usuário Provedor de teste Layout diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 46bd860d..275553a9 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -2,7 +2,7 @@ - %s Ep %d + %1$s Ep %2$d Hrají: %s Plakát @@ -17,7 +17,7 @@ Rychlost (%.2fx) Hodnocení: %.1f Nalezena nová aktualizace! -\n%s -> %s +\n%1$s -> %2$s Výplň %d min CloudStream @@ -40,7 +40,6 @@ Dokončeno Zahozeno Plánuji sledovat - Žádné Opětovné sledování Přehrát film Streamovat torrent @@ -76,7 +75,6 @@ Odebrat Nastavit stav sledování Použít - Zrušit Kopírovat Zavřít Vymazat @@ -176,6 +174,7 @@ Nenalezeny žádné epizody Smazat soubor Smazat + Zrušit Pozastavit Pokračovat -30 @@ -194,7 +193,7 @@ Synopse ve frontě Žádné titulky - Výchozí + Výchozí Volné Použito Aplikace @@ -293,7 +292,7 @@ Kitsu Trakt --> - %s %s + %1$s %2$s účet Odhlásit se Přihlásit se @@ -411,17 +410,17 @@ Příliš mnoho textu. Nepodařilo se uložit do schránky. Ano Prohlížeč - %d-%d + %1$d-%2$d Knihovna Zobrazit plakáty z Kitsu Automaticky stahovat doplňky Znovu provést proces nastavení Instalátor APK - %d %s + %1$d %2$s Některé telefony nepodporují nový instalátor balíčků. Pokud se aktualizace nenainstalují, zkuste použít starší možnost. Mezipaměť Epizoda %d bude vydána za - %dh %dm + %1$dh %2$dm Přehrát přímý přenos Rozšíření Akce @@ -437,7 +436,7 @@ Co chcete vidět Doplněk stažen 18+ - Spuštěno stahování %d %s… + Spuštěno stahování %1$d %2$s… CloudStream nemá ve výchozím nastavení nainstalované žádné weby. Stránky je třeba nainstalovat z úložišť. \n \nKvůli nesmyslnému podání stížnosti DMCA společností Sky UK Limited 🤮 nemůžeme v aplikaci propojit stránky repozitářů. @@ -506,12 +505,12 @@ Aktualizace aplikace Hotovo Podporováno - %s %d%s + %1$s %2$d%3$s Živý přenos NSFW Rozšíření Přehrát trailer - %dd %dh %dm + %1$dd %2$dh %3$dm Zobrazit komunitní repozitáře Aktualizace zahájena Stream @@ -521,7 +520,7 @@ Referent Další Sledovat videa v těchto jazycích - Staženo %d %s + Staženo %1$d %2$s Všechny %s jsou již staženy Hromadné stahování doplněk @@ -575,6 +574,32 @@ Výběr režimu pro filtrování stahování doplňků V repozitáři nebyly nalezeny žádné doplňky Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN - @string/default_subtitles Již jste hlasovali + %s odebráno z oblíbených + Oblíbené + %s přidáno do oblíbených + Ve vaší knihovně byl nalezen potenciální duplikát: +\n +\n%s +\n +\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci\? + Frekvence záloh + Nalezena potenciální duplicita + Zamknout profil + Přidat do oblíbených + Nahradit vše + Nesprávný PIN. Zkuste to prosím znovu. + Zrušit odběr + PIN musí obsahovat 4 znaky + Nahradit + Přidat + Odebírat + Odebrat z oblíbených + Vyberte účet + Vypadá to, že ve vaší knihovně již existuje potenciální duplikát: „%1$s“. +\n +\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci\? + Zadejte PIN + PIN + Zadejte současný PIN diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 85d9ab71..bc3119a3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -52,7 +52,6 @@ Abgeschlossen Abgebrochen Geplant - Nichts Erneut schauen Film abspielen Livestream abspielen @@ -89,7 +88,6 @@ Entfernen Status setzen Anwenden - Abbrechen Kopieren Schließen Leeren @@ -192,6 +190,7 @@ E Keine Episoden gefunden Löschen + Abbrechen Pause Fortsetzen -30 @@ -210,7 +209,7 @@ Zusammenfassung In Warteschlange eingereiht Keine Untertitel - Standard + Standard Frei Belegt App @@ -552,5 +551,4 @@ Repository nicht gefunden, überprüf die URL und versuch ein VPN Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s Deaktivieren - @string/default_subtitles diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e88e4fc0..72a7c4c9 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -23,7 +23,6 @@ Ολοκληρώθηκε Διακόπηκε Για παρακολούθηση - Τίποτα Αναπαραγωγή ταινίας Μετάδοση Torrent Πηγές @@ -58,7 +57,6 @@ Αφαίρεση Αναπαραγωγή επεισοδίου Υποβολή - Ακύρωση Ταχύτητα αναπαραγωγής Ρυθμίσεις υπότιτλων Χρώμα κειμένου @@ -155,6 +153,7 @@ Δεν βρέθηκαν επεισόδια Διαγραφή αρχείου Διαγραφή + Ακύρωση Παύση Συνέχιση Αυτό θα διαγράψει μόνιμα το %s @@ -169,7 +168,7 @@ Περίληψη προστέθηκε στην ουρά Δεν υπάρχουν διαθέσιμοι υπότιτλοι - Προεπιλεγμένοι υπότιτλοι + Προεπιλεγμένοι υπότιτλοι Ελεύθερος Σε χρήση Εφαρμογή @@ -547,6 +546,5 @@ Το UI δεν ήταν σε θέση να δημιουργηθεί σωστά, είναι ένα σφάλμα και θα πρέπει να αναφερθεί αμέσως %s Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη Απενεργοποιημένο - @string/default_subtitles Τέλος diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 49f025d0..6dcf12c3 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -22,7 +22,6 @@ Priskribo Versio Stato - Nuligi Forviŝi Jes Ne diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 51063b4d..32e2b61d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -4,10 +4,10 @@ Descargue la lista de sitios que quiera utilizar Descargado:%d Descargado - Descargado %d %s + Descargado %1$d %2$s Borrar repositorio El episodio %d se lanzará en - %dh %dm + %1$dh %2$dm %dm Poster Extensiones @@ -45,7 +45,7 @@ Cargar desde Internet Reproducir automáticamente episodio siguiente Configuración de subtítulos de Chromecast - Predeterminado + Predeterminado Contorno Sin Subtítulos Elevado @@ -97,12 +97,12 @@ Poster Siguiente al azar Todos los Idiomas - Volver + Regresar Cambiar proveedor Vista previa del fondo Nota:%.1f - Nueva actualización encontrada! -\n%s -> %s + ¡Nueva actualización encontrada! +\n%1$s -> %2$s Descargar Pausar Descarga Formato de fuente @@ -110,8 +110,8 @@ Tamaño de Fuente Velocidad (%.2fx) Omitir carga - %s Ep %d - %dd %dh %dm + %1$s Ep. %2$d + %1$dd %2$dh %3$dm Elenco %s Relleno %d min @@ -138,7 +138,6 @@ Completado Descartado Planeando ver - Ninguno Volviendo a mirar Reproducir película Reproducir Trailer @@ -146,7 +145,7 @@ Transmitir Torrent Fuentes Reintentar conexión… - Volver + Regresar Descargando Descarga pausada Descarga iniciada @@ -167,7 +166,6 @@ Marcadores Remover Seleccionar estado de visualización - Cancelar Copiar Cerrar Limpiar @@ -220,8 +218,8 @@ Reproducir Episodio Episodio Episodios - %d-%d - %d %s + %1$d-%2$d + %1$d %2$s E Falló la restauración de los datos desde el archivo %s Datos guardados @@ -235,7 +233,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 Trailers (avances) + Mostrar los trailers Mostrar pósters de Kitsu Actualizar a las versiones preliminares Buscar actualizaciones preliminares (beta) en lugar de solo versiones completas (stable releases) @@ -251,11 +249,12 @@ Reiniciar a valores predefinidos Lo sentimos, la aplicación se bloqueó. Se enviará un informe de error anónimo a los desarrolladores Temporada - %s %d%s + %1$s %2$d%3$s Ninguna Temporada T Borrar Archivo Borrar + Cancelar Error inesperado del reproductor Episodio en Chromecast Reproducir en la aplicación @@ -312,7 +311,7 @@ Cambiar cuenta Añadir cuenta Sincronizar - Calificación + Clasificado %s autenticado No se pudo autenticar a %s Recomendado @@ -346,7 +345,7 @@ Color primario Tema de la aplicación hola@mundo.com - %s %s + %1$s %2$s Todo 1000ms Sin retraso de subtítulos @@ -449,7 +448,7 @@ Hecho Plugin Cargado 18+ - Iniciada la descarga %d %s… + Comenzó la descarga de %1$d %2$s… Descarga por lotes plugin plugins @@ -549,9 +548,34 @@ 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 - @string/default_subtitles No se encontraron complementos en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN Ya has votado Frecuencia de la copia de seguridad + %s eliminado de favoritos + Favoritos + %s añadido a favoritos + Se han encontrado posibles elementos duplicados en su biblioteca: +\n +\n%s +\n +\n¿Desea añadir este elemento de todos modos, sustituir los existentes o cancelar la acción\? + Posible duplicado encontrado + Perfil de bloqueo + Añadido a favoritos + Sustituir todo + PIN incorrecto. Por favor, inténtelo de nuevo. + Cancelar la suscripción + El PIN debe tener 4 caracteres + Sustituir + Añadir + Suscríbase + Eliminar de favoritos + Seleccione una cuenta + Parece que ya existe un elemento potencialmente duplicado en su biblioteca: \'%s.\' +\n +\n¿Desea añadir este elemento de todos modos, sustituir el existente o cancelar la acción\? + Introducir el PIN + PIN + Introduzca el PIN actual diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 53442e84..2530a068 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -22,7 +22,6 @@ Terminé Abandonné À regarder - Aucun Lire Streamer le Torrent Sources @@ -60,7 +59,6 @@ Marque-pages Supprimer Appliquer - Annuler Vitesse de lecture Aperçu de l\'arrière-plan Donner une benene aux devs @@ -80,6 +78,7 @@ E Supprimer le Fichier Supprimer + Annuler Pause Reprendre Cela va supprimer définitivement %s @@ -94,7 +93,7 @@ Synopsis Liste d\'attente Pas de sous-titres - Défault + Défault Libre Utilisé Application @@ -553,5 +552,4 @@ L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins Fond de profil - @string/default_subtitles diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index ad7916be..92754082 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -29,13 +29,11 @@ Completado Descartado Planeando ver - Ningún Remirando Marcadores Borrar Seleccionar estado de visualización Aplicar - Cancelar Copiar Cerrar Limpar diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 31e0e28c..5053544d 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -49,7 +49,6 @@ बुकमार्क्स हटाएँ लागू करें - रद्द करें प्लेयर स्पीड प्रोवाइडरों का उपयोग कर खोजें प्रकार का उपयोग करके खोजें @@ -91,6 +90,7 @@ क्षमा करें, एप्प क्रैश हो गया है । निर्माताओं को एक अनाम बग रिपोर्ट भेजी जाएगी फ़ाइल डिलीट करें डिलीट + रद्द करें रोकें फिर से चलाएं इससे %s स्थायी रूप से हट जाएगा diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 84915a38..8da04be6 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -54,7 +54,6 @@ Dovršeno Ispušteno Planiram pogledati - Ništa Ponovno gledam Pokreni Film Pokreni LiveStream @@ -92,7 +91,6 @@ Ukloni Postavi status gledanja Primijeni - Poništi Kopiraj Zatvori Očisti @@ -200,6 +198,7 @@ Nisu pronađene epizode Izbriši datoteku Izbriši + Poništi Pauziraj Nastavi -30 @@ -218,7 +217,7 @@ Sinopsis u redu čekanja Bez titlova - Zadano + Zadano Slobodno Iskorišteno Aplikacija @@ -567,7 +566,6 @@ Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka Onemogući - @string/default_subtitles U repozitoriju nisu pronađeni dodaci Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 677beaf8..4cd322a9 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -20,6 +20,7 @@ Letöltés Keresés Törlés + Mégse Szüneteltetés sorba állítva Igazítás @@ -61,7 +62,6 @@ Nézés Befejezve Később megnézés - Nincs Újranézés Film lejátszása Előzetes lejátszása @@ -90,7 +90,6 @@ Eltávolítás Megtekintés állapotának beállítása Alkalmazás - Mégse Másolás Bezárás Törlés @@ -162,7 +161,7 @@ Értékelés Rajzfilmek Élőadások - Alapértelmezett + Alapértelmezett Filmek TV sorozat Anime diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index d514bcc4..a6c5813d 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -39,7 +39,6 @@ Selesai Dihentikan Rencana untuk Menonton - Tidak Ada Menonton Ulang Putar Movie Streaming Torrent @@ -75,7 +74,6 @@ Hapus Atur status tontonan Terapkan - Batalkan Salin Tutup Bersihkan @@ -174,6 +172,7 @@ Episode Tidak Ditemukan Hapus File Hapus + Batalkan Jeda Lanjutkan -30 @@ -192,7 +191,7 @@ Sinopsis antri Tidak Ada Subtitle - Default + Default Tersedia Terpakai Aplikasi @@ -575,5 +574,4 @@ Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN Kamu sudah voting - @string/default_subtitles diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0c34e89a..d6be0eed 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -20,7 +20,7 @@ Velocità (%.2fx) Valutato: %.1f Nuovo aggiornamento trovato! -\n%s -> %s +\n%1$s -> %2$s Filler %d min @@ -44,7 +44,6 @@ Completato Abbandonato Da guardare - Nessuno Riguardando Riproduci film Riproduci Livestream @@ -81,7 +80,6 @@ Rimuovi Imposta stato riproduzione Applica - Cancella Copia Chiudi Cancella @@ -190,6 +188,7 @@ Nessun episodio trovato Elimina file Elimina + Cancella Pausa Riprendi -30 @@ -208,7 +207,7 @@ Sinossi In coda Nessun sottotiolo - Default + Default Libero Usato App @@ -410,8 +409,8 @@ Plugin eliminato Impossibile caricare %s 18+ - Download iniziato %d %s… - Scaricato %d %s + Download iniziato %1$d %2$s… + Scaricato %1$d %2$s Tutti %s già scaricati Download in blocco plugin @@ -572,7 +571,32 @@ Repository non trovato, controlla l\'URL e prova la VPN Non è stato possibile creare correttamente l\'interfaccia utente, questo è un GRANDE BUG e dovrebbe essere segnalato immediatamente %s Seleziona la modalità per filtrare il download dei plugin - @string/default_subtitles Disabilita Hai già votato + %s rimosso dai preferiti + Preferiti + %s aggiunto ai preferiti + Dei possibili duplicati sono stati trovati nella tua libreria: +\n +\n%s +\n +\nVorresti aggiungere l\'oggetto alla libreria comunque, rimpiazzare l\'esistente, o cancellare l\'azione\? + Frequenza di backup + Trovato Possibile Duplicato + Aggiungi ai preferiti + Rimpiazza tutti + PIN non corretto. Riprova. + Disiscriviti + Il PIN deve essere almeno di 4 caratteri + Rimpiazza + Aggiungi + Iscriviti + Rimuovi dai preferiti + Seleziona un Account + Sembra che un oggetto potenziale duplicato sia già presente nella tua libreria: \'%1$s.\' +\n +\nVorresti aggiungere l\'oggetto lo stesso, rimpiazzare l\'esistente, o cancellare l\'azione\? + Inserisci PIN + PIN + Inserisci PIN Corrente diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d6d5b7f6..55666df5 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -66,7 +66,6 @@ הסר הגדר מצב צפייה ליישם - בטל העתק לסגור נקה @@ -78,7 +77,6 @@ צופה כתוביות בהמתנה - ללא להוריד מדובב יותר מידע @@ -146,6 +144,7 @@ לא נמצאו פרקים מחק קובץ מחק + בטל השהה המשך -30 @@ -159,7 +158,7 @@ דירוג שנה ללא כתוביות - ברירת מחדל + ברירת מחדל חינם משומש סדרת טלוויזיה @@ -519,7 +518,6 @@ עריכה Wi-Fi רקע הפרופיל - @string/default_subtitles רשומה עזרה התחלה diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7f10aad4..16341b60 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -37,7 +37,6 @@ アジアドラマ ライブ配信 NSFW - キャンセル アニメ ロック ソース @@ -71,7 +70,6 @@ ソース 履歴 ポスター - なし コピー 閉じる 保存 @@ -135,6 +133,7 @@ 一時停止 再生エピソード 削除 + キャンセル 開始 状態 @@ -155,7 +154,7 @@ バージョン 視聴率 %s 視聴率 - デフォルト + デフォルト ダウンロード失敗 ダウンロード開始 ダウンロード完了 diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 399aafb1..07ff89e4 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -32,7 +32,6 @@ ಮಾಹಿತಿ ಸೆಟ್ ವಾಚ್ ಸ್ಟೇಟಸ್ ಅನ್ವಯಿಸು - ರದ್ದುಮಾಡು ಸಬ್ ಟೈಟಲ್ಸ್ ಎಲೆವಷನ್ ಫಾಂಟ್ ಸೈಜ್ ಸಬ್ ಟೈಟಲ್ಸ್ ಭಾಷೆ @@ -108,7 +107,6 @@ ಪ್ರಕಾರಗಳು ಬ್ರೌಸರ್ ತೆರೆಯಿರಿ ಆನ್-ಹೋಲ್ಡ್ - ನನ್ ಸಂಪರ್ಕವನ್ನು ಮರುಪ್ರಯತ್ನಿಸಿ… ಡೌನ್‌ಲೋಡ್ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ ಡೌನ್‌ಲೋಡ್ ವಿಫಲವಾಗಿದೆ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 74c05d07..ef2f6cc5 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -35,7 +35,6 @@ 시청 완료 포기 시청 예정 - 없음 다시보기 영화 재생 예고편 재생 @@ -95,7 +94,6 @@ 백업 더빙 자막 - 취소 북마크 필터 북마크 제거 @@ -177,7 +175,7 @@ 개요 대기중 자막 없음 - 기본 + 기본 남음 사용됨 @@ -352,6 +350,7 @@ 아시아 드라마 시즌 삭제 + 취소 %s %d%s 파일 삭제 일시정지 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index a702a62f..db5ac011 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -65,6 +65,7 @@ Dokumentika Transliuoti Torrentą Ištrinti + Atšaukti Pradėti Prideda greičio pasirinkti grotuve Filmukas @@ -95,7 +96,6 @@ Teksto spalva Užbaigta Naudoti sistemos ryškumą programos grotuve vietoj tamsumo - Tuščia Nepavyko atstatyti duomenis iš failo %s Paleisti anonsa Paleisti gyva transliacija @@ -107,7 +107,6 @@ Peržiūrima Atidaryti Naršyklėje Naudoti sistemos ryškumą - Atšaukti Nėra duomenų Šriftas Perdaryti nustatymo procesą diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index ddd39942..14e5b600 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -31,7 +31,6 @@ Pabeigts Atmests Plāno skatīties - Neviena Atkārtoti skatities Palaist Filmu Palaist Trelleri @@ -67,7 +66,6 @@ Noņemt Ieliec skatīšanās statusu Izmantot - Atcelt Kopēt Saglabāt Atskaņošanas ātrums @@ -192,6 +190,7 @@ Epizodes netika atrastas Dzēsti faili Dzēst + Atcelt Pauzēt Sākt Neizdevās @@ -483,7 +482,7 @@ Iet Bezmaksas Ieslēgt elementus uz plakātiem - Parastais + Parastais Nav atjauninājumi atrasti Izdzēst video un bildes atkritne Izvēlētā skatīšanās kvalitāte (Mobilie Dati) diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 66a6b9ba..7badfd18 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -25,7 +25,6 @@ Завршени Отфрлени Планирани за гледање - Ништо одбрано Повторно гледање Пушти филм Стримај торент @@ -58,7 +57,6 @@ Обележувачи Отстрани Активирај - Откажи Брзина на плеер Поставки за преводи Боја на текстот @@ -133,6 +131,7 @@ Е Избриши датотека Избриши + Откажи Паузирај Продолжи Ова трајно ќе го избрише %s @@ -147,7 +146,7 @@ Крат во редица Нема преводи - Стандардно + Стандардно Слободен простор Искористен простор Апликациски простор diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 3d6240f9..cdc50cea 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -54,7 +54,6 @@ ബുക്മാർക് നീക്കം ചെയ്യുക പ്രയോഗിക്കുക - റദ്ദാക്കുക പ്ലേയർ വേഗത + Default --> ഒഴിവ് ഉപയോഗത്തിൽ ആപ്പ് @@ -197,7 +197,6 @@ ദാതാവിനെ മാറ്റുക ലോഡിംഗ്… ബ്രൗസർ - ഒന്നുമില്ല വീണ്ടും കാണുക സ്ട്രീം diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 222bef61..e579381e 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -49,7 +49,6 @@ Buka dengan Padam Fail Buka Dalam Pelayar - Batal Tiada Data Info Simpan diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 0cb44373..e0346f19 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -32,7 +32,6 @@ ကြည့်နေသည် ကြည့်ပြီး ကြည့်ခြင်းရပ်ထားသော - ဘာမျှ လင့်များချိတ်ဆက်ရာတွင်အချို့အယွင်း ဖုန်း သိုလှောင်ရုံ စာမှတ်များ စစ်ထုတ်မှု @@ -127,8 +126,7 @@ အကျဥ်းချုပ် နောက်အစီအစဥ် စာတန်းထိုးမထည့် - ပုံသေ - @string/default_subtitles + ပုံသေ ကျန်ရှိသော အက်ပ် ရုပ်ရှင်များ @@ -253,7 +251,6 @@ စာတန်းထိုး ဘာသာစကား ဒီမှာနေရာချခြင်းဖြင့်ဖောင့်များကိုသွင်းပါ %s အတည်ပြု - ပယ်ဖျက်ရန် စာတန်းထိုး ပြုပြင်ခြင်း စာသား အရောင် အနားကွပ် အရောင် @@ -301,6 +298,7 @@ အပိုင်းများမတွေ့ပါ ဖိုင်ကိုဖျက်ရန် ဖျက်ရန် + ပယ်ဖျက်ရန် ရပ်ရန် စရန် မအောင်မြင်ပါ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d19726fd..d73d77a0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -44,7 +44,6 @@ Voltooid Dropped Plan om te kijken - Geen Opnieuw kijken Film afspelen Livestream afspelen @@ -82,7 +81,6 @@ Verwijder Zet kijkstatus Toepassen - annuleer Kopiëren Sluit Wissen @@ -186,6 +184,7 @@ Geen afleveringen gevonden Verwijder bestand Verwijder + annuleer Pauze Hervatten -30 @@ -204,7 +203,7 @@ Korte inhoud wachtrij Geen ondertiteling - Standaard + Standaard Vrij Gebruikt App @@ -573,6 +572,5 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - @string/default_subtitles Je hebt al gestemd diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 592ff22c..c70ebd4b 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -63,7 +63,6 @@ Fjern Sett visingstatus Bruk - Avbryt Kopier Tøm Lagre @@ -113,6 +112,7 @@ Ingen episodar blei funnen Slett fil Slett + Avbryt Pause Gjenoppta -30 @@ -129,7 +129,7 @@ Om i kø Ingen undertekstar - Standard + Standard Brukt Program Filmar @@ -166,7 +166,6 @@ Gå tilbake Sjangrar Dele - Ingen Ser om igjen Oppdatering starta Fortsett nedlasting diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index dac15d61..9845120b 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -33,7 +33,6 @@ Fullført Falt Planlegg å se - Ingen Spill filmer Strøm Torrent Kilder @@ -67,7 +66,6 @@ Bokmerker Ta bort Søke om - Avbryt Spillerhastighet Innstillinger for teksting Tekstfarge @@ -141,6 +139,7 @@ E Slett fil Slett + Avbryt Stopp Gjenoppta Dette vil slette %s @@ -155,7 +154,7 @@ Om I kø Ingen undertekster - Misligholde + Misligholde Tilgjengelig Brukt applikasjon diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 63dba53d..22f7d1ca 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -19,7 +19,6 @@ ଵେଗ (%.2fଗୁଣ) ତ୍ୟାଗିଛନ୍ତି ଦେଖିବା ପାଇଁ ଇଚ୍ଛୁକ - କିଛି ନାହିଁ ଅଧିକ ସୂଚନା ପାତ୍ର: %s ପୋଷ୍ଟର୍ @@ -42,7 +41,7 @@ ଅଧ୍ୟାୟର ପୋଷ୍ଟର୍ ମୁଖ୍ୟ ପୋଷ୍ଟର୍ - ଡିଫଲ୍ଟ + ଡିଫଲ୍ଟ ଭାଷା ନାହିଁ ଵର୍ଣ୍ଣନା diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a170d610..95a3e1c6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -35,7 +35,6 @@ Zakończone Porzucone Planowane - Brak Ponowne oglądanie Odtwórz film Odtwórz transmisję na żywo @@ -72,7 +71,6 @@ Usuń Ustaw status oglądania Zastosuj - Anuluj Kopiuj Zamknij Wyczyść @@ -181,6 +179,7 @@ Nie znaleziono odcinków Usuń plik Usuń + Anuluj Wstrzymaj Odtwórz -30 @@ -199,7 +198,7 @@ Streszczenie W kolejce Brak napisów - Domyślne + Domyślne Wolne W użyciu Aplikacja @@ -552,7 +551,6 @@ Nie można było poprawnie utworzyć interfejsu użytkownika, jest to POWAŻNY BŁĄD i należy go natychmiast zgłosić %s Wybierz tryb filtrowania pobieranych rozszerzeń Wyłączać - @string/default_subtitles Nie znaleziono żadnych wtyczek w repozytorium Już oddano głos Nie znaleziono tego repozytorium, sprawdź adres URL lub spróbuj połączyć się przez VPN diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 908ddb0d..c73fb996 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -42,7 +42,6 @@ Concluído Desistido Pretendo assistir - Nenhum Reassistindo Reproduzir filme Reproduzir transmissão ao vivo @@ -78,7 +77,6 @@ Marcadores Remover Aplicar - Cancelar Copiar Fechar Limpar @@ -182,6 +180,7 @@ Nenhum Episódio encontrado Eliminar Ficheiro Eliminar + Cancelar Pôr em Pausa Retomar Isto apagará %s permanentemente @@ -198,7 +197,7 @@ Sinopse Na fila Sem Legendas - Padrão + Padrão Livre Usado App @@ -548,7 +547,6 @@ \nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! Selecionar o modo para filtrar a transferência de plug-ins Não foi possível criar corretamente a interface do utilizador, trata-se de um GRANDE BUG e deve ser comunicado imediatamente %s - \@ string/legendas_padrão Desativar Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 9c68c008..e1191d2b 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -23,7 +23,6 @@ ahhahooo ooooo haa aaahhuoh - aaaghh aaaaa aaaghh aaaghhaaahhu aaaaa oha aauuh @@ -51,7 +50,6 @@ oouuh ooh aauuh ooh oouuhaooo-ahah - oooohh oouuh aauuh oha ouuhhhooooooh ooo-ahah ohaouuhhh @@ -122,6 +120,7 @@ A ooha ohahaaahoooa ahahooo oooooah + oooohh aahhaaaaaahooo aauuh aaahhuaooo-ahahoooohh aoouuhoohoohooo-ahah %s aaaghhaaaaa aauuhaauuh @@ -133,7 +132,7 @@ aauugghh ohaaauugghh haaouuhhh ahaaaaaaaaaahaaaauugghh - ahooooh + ahooooh ooo-ahahaaaaa ahhahhh aauugghh diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index b6971c37..75388b23 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -43,7 +43,6 @@ Finalizat Renunțat Planificare pentru a urmări - Anuleaza Reurmat Urmărește Stream Torrent @@ -79,7 +78,6 @@ Eliminează Adaugă Aplică - Anulează Copiază Închide Elimină @@ -181,6 +179,7 @@ Nu s-au găsit episoade Ștergeți fișierul Ștergeți + Anulează Pauză Continuă -30 @@ -199,7 +198,7 @@ Rezumat În coada de așteptare Nu există subtitrare - Implicit + Implicit Liber Folosit Aplicație @@ -569,6 +568,5 @@ Utilizați UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor - @string/default_subtitles Ați votat deja diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e7b85b3d..64c3146e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -12,6 +12,7 @@ Скачать неудачный Подогнать Удалить + Отмена Все Пауза Актёрский состав: %s @@ -58,7 +59,6 @@ Завершено Брошенный План посмотреть - Нет Пересмотрю Смотреть фильм Смотреть трейлер @@ -82,7 +82,6 @@ Фильтр закладки Закладки Применить - Отмена Копия Закрыть Очистить @@ -186,7 +185,7 @@ Рейтинг Продолжительность Нет субтитров - По умолчанию + По умолчанию Приложение Аниме Торренты @@ -535,7 +534,6 @@ Изменить Интернет Задний фон профиля - \@строка/обычные_субтитры Помощь Профиль %d Выберите режим фильтера плагинов для загрузки diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index e0cc27d0..4f17971e 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -33,7 +33,6 @@ Hľadať… Sťahovanie Žiadne dáta - Zrušiť Kopírovať Zavrieť Uložiť @@ -60,7 +59,6 @@ Sťahovanie zrušené Dab Zmazať súbor - Žiadny Tit Opätovné sledovanie Prehrať súbor @@ -265,7 +263,7 @@ Preferované médiá URL servera NGINX %d %s - Predvolené + Predvolené Pridať sledovanie Žiadna sezóna Epizóda @@ -293,6 +291,7 @@ Rok Prispôsobiť obrazovke Zmazať + Zrušiť Využité Štítok kvality Prehrávač skrytý - dĺžka pretočenia diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index db82d9fa..c3d107f5 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -10,6 +10,7 @@ Dejintii ma guulaysan Raadi Tirtir + Jooji Haki Horran Le-ekaysii shaashadda @@ -46,7 +47,6 @@ Dhamaystirmay Soo dhacay Ku talo jira - Midna Daaro filinka Daar goos-gooska Midabka daaqadda @@ -56,7 +56,6 @@ Daalaco toorentiga Qrl-hoosaadka Dib u bilow dejinta - Jooji Midabka dambeedka Xigashooyinka Dib u xidhiidhinaya… @@ -202,7 +201,7 @@ Muddada La isticmaalay Qrl-hoosaad ma leh - Sidiisaa + Sidiisaa Bilaash Appka Kartoob diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 397faa48..806a1ca3 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -26,7 +26,6 @@ Avslutad Dropped Plannerad - Ingen Spela Upp Strömma Torrent Källor @@ -53,7 +52,6 @@ Bokmärken Ta bort Tillämpa - Avbryt Spelarhastighet Undertextinställningar Textfärg @@ -125,6 +123,7 @@ A Ta bort nerladdad fil Ta bort + Avbryt %s kommer att raderas permanent \nÄr du helt säker\? Pågående @@ -137,7 +136,7 @@ Sammanfattning på kö Inga undertexter - Standard + Standard Tillgängligt Använtt App diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 9c9a335b..ca4a1377 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -38,7 +38,6 @@ மேலும் தகவல்கள் மறை நீக்கு - ரத்து செய்க நீக்கு சேமிக்கவும் உரை வண்ணம் @@ -63,7 +62,6 @@ நடிகர்கள்: %s பின் செல் அமைப்புகள் - ஏதும் இல்லை ஏற்றுகிறது… கைவிடப்பட்டது பதிவிறக்கம் முடிந்தது diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 95d38478..03138259 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -36,7 +36,6 @@ Tapos nang panoorin Ayaw nang panoorin Balak panoorin - None I-play ang Movie Stream Torrent Sources @@ -70,7 +69,6 @@ Bookmark Tanggalin Kumpirmahin - Kanselahin Bilis ng Playback Subtitle Setting Kulay ng Teksto @@ -144,6 +142,7 @@ E Burahin ang file Tanggalin + Kanselahin I-pause I-resume This will permanently delete %s @@ -158,7 +157,7 @@ Sinopsis nakapila walang subtitles - Default + Default Bakante Gamit App diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e0d7e4cd..c1e06a7b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -57,7 +57,6 @@ Tamamlandı Bırakıldı Planlandı - Hiçbiri Yeniden izleniyor Filmi oynat Canlı yayını oynat @@ -96,7 +95,6 @@ Kaldır İzleme durumunu ayarla Uygula - İptal et Kopyala Kapat Temizle @@ -205,7 +203,7 @@ Bölüm bulunamadı Dosyayı sil Sil - @string/sort_cancel + İptal et Durdur Sürdür -30 @@ -224,7 +222,7 @@ Özet Sıraya alındı Alt yazı yok - Varsayılan + Varsayılan Boş Kullanılan Uygulama diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index c069cae0..425c5257 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -9,16 +9,16 @@ Актори: %s Епізод %d вийде через Poster - %s Еп. %d - %dд %dгод %dхв - %dгод %dхв + %1$s Еп. %2$d + %1$dд %2$dгод %3$dхв + %1$dгод %2$dхв %dхв Головний постер Наступний випадковий Попередній перегляд фону Швидкість (%.2fx) Знайдено нове оновлення! -\n%s –> %s +\n%1$s –> %2$s Пошук Завантаження %d хв @@ -112,8 +112,6 @@ Переглянути файл Детальніше Фільтр закладок - Нічого - Скасувати Очистити Налаштування субтитрів Колір фону @@ -169,11 +167,12 @@ Скинути до значення за замовчуванням Немає сезону епізодів - %d %s + %1$d %2$s С Е Видалити файл Видалити + Скасувати Відновити -30 Це назавжди видалить %s @@ -186,7 +185,7 @@ Тривалість у черзі Без субтитрів - За замовчуванням + За замовчуванням Вільно Зайнято Застосунок @@ -239,9 +238,9 @@ Рік +30 Вибачте, у застосунку стався збій. Анонімне повідомлення про помилку буде відправлено розробникам - %s %d%s + %1$s %2$d%3$s Епізод - %d-%d + %1$d-%2$d Епізодів не знайдено Пауза Сезон @@ -365,7 +364,7 @@ Макет Постачальники example.com - %s %s + %2$s %1$s Депресивний обліковий запис Створити @@ -415,8 +414,8 @@ Плагін завантажено Плагін завантажено Не вдалося завантажити %s - Почалося завантаження %d %s… - Завантажено %d %s + Почалося завантаження %1$d %2$s… + Завантажено %1$d %2$s Всі %s вже завантажено Завантажити пакети плагін @@ -500,7 +499,7 @@ Ваша бібліотека порожня :( \nУвійдіть в обліковий запис бібліотеки або додайте фільми до вашої локальної бібліотеки. Алфавітом (від Я до А) - Виберіть бібліотеку + Оберіть бібліотеку Відкрити Браузер Цей список порожній. Спробуйте перейти до іншого. @@ -547,11 +546,36 @@ Якості Фон профілю Не вдалося створити UI коректно, це ВАЖЛИВА ПОМИЛКА, про яку слід негайно повідомити %s - Виберіть режим для фільтрації завантаження плагінів + Оберіть режим для фільтрації завантаження плагінів Вимкнути - @string/default_subtitles Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували Частота резервного копіювання + %s вилучено з обраного + Обране + %s додано до обраного + У вашій бібліотеці знайдено потенційні дублікати: +\n +\n%s +\n +\nВсе одно хочете додати цей елемент, замінити наявні чи скасувати дію\? + Знайдено потенційний дублікат + Розблокувати профіль + Додати до обраного + Замінити усе + Неправильний PIN-код. Спробуйте ще раз. + Відписатись + PIN-код повинен складатися з 4 символів + Замінити + Додати + Підписатись + Вилучити з обраного + Оберіть обліковий запис + Виявилося, що у вашій бібліотеці вже є потенційно повторюваний елемент: \'%1$s.\' +\n +\nВсе одно хочете додати цей елемент, замінити наявний чи скасувати дію\? + Введіть PIN-код + PIN-код + Введіть поточний PIN-код diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index b9585d07..b437e2d0 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -34,7 +34,6 @@ معطل چھوڑ دیا گیا دیکھنے کا منصوبہ - کوئی نہیں دوبارہ دیکھنا مووی لگائے ٹریلر چلائیں @@ -67,7 +66,6 @@ ریمو واچ اسٹیٹس کو سیٹ کریں لاگو کریں - منسوخ کریں کاپی بند کریں صاف کریں @@ -193,6 +191,7 @@ کوئی اقساط نہیں ملی فائل کو ڈیلیٹ کریں مٹا دیں + منسوخ کریں توقف از سر نو شروع کریں -30 @@ -211,7 +210,7 @@ خلاصہ قطار میں کوئی سب ٹائٹلز نہیں - ڈیفالٹ + ڈیفالٹ مفت استعمال شُدہ کارٹون @@ -535,7 +534,6 @@ ترتیب دیں وائی فائی پروفائل پس منظر - @string/default_subtitles مدد پروفائل %d پلگ انز کو ڈاؤن لوڈ کرنے کے لئے موڈ منتخب کریں diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 217d2791..d27660c7 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -45,7 +45,6 @@ Đã xem Bỏ qua Xem sau - Mặc định Xem lại Xem Ngay Phát trực tiếp @@ -83,7 +82,6 @@ Xóa Đặt trạng thái xem Áp dụng - Hủy bỏ Sao lưu Đóng Huỷ bỏ @@ -190,6 +188,7 @@ Không có tập nào Xóa Tệp Xóa + Hủy bỏ Tạm Dừng Tiếp Tục -30 @@ -208,7 +207,7 @@ Thông tin Hàng chờ Không có phụ đề - Mặc Định + Mặc Định Còn trống Đã sử dụng App @@ -410,8 +409,8 @@ Plugin đã xoá Không tải được %s 18+ - Bắt đầu tải %d %s… - Tải xuống %d %s thành công + Đã bắt đầu tải xuống %1$d %2$s… + Đã tải xuống %1$d %2$s Toàn bộ %s đã được tải xuống Tải hàng loạt plugin @@ -567,5 +566,31 @@ Không tìm thấy plugin Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s Chọn chế độ để lọc plugin tải xuống - @string/default_subtitles + %s đã loại bỏ khỏi mục yêu thích + Yêu thích + %s đã thêm vào mục yêu thích + Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn: +\n +\n%s +\n +\nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động\? + Tần suất sao lưu + Đã tìm thấy bản sao tiềm năng + Khóa hồ sơ + Thêm vào mục yêu thích + Thay thế tất cả + Mã PIN không chính xác. Vui lòng thử lại. + Hủy đăng ký + Mã PIN phải có 4 ký tự + Thay thế + Thêm vào + Đăng ký + Loại bỏ khỏi mục yêu thích + Chọn một tài khoản + Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%1$s.\' +\n +\nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động\? + Nhập PIN + PIN + Nhập mã PIN hiện tại diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1fd01d8a..52897633 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -57,7 +57,6 @@ 觀看完畢 放棄觀看 計畫觀看 - 重新觀看 播放電影 播放直播 @@ -96,7 +95,6 @@ 移除 設定觀看狀態 套用 - 取消 複製 關閉 清除 @@ -205,7 +203,7 @@ 未找到劇集 刪除文件 刪除 - @string/sort_cancel + 取消 暫停 繼續 -30 @@ -224,7 +222,7 @@ 簡介 已加入佇列 無字幕 - 預設 + 預設 空閒 已使用 應用程式 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 033a5f50..682bcab7 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -57,7 +57,6 @@ 观看完毕 放弃观看 计划观看 - 重新观看 播放电影 播放直播 @@ -96,7 +95,6 @@ 移除 设置观看状态 应用 - 取消 复制 关闭 清除 @@ -206,7 +204,7 @@ 未找到剧集 删除文件 删除 - @string/sort_cancel + 取消 暂停 继续 -30 @@ -225,7 +223,7 @@ 简介 已加入队列 无字幕 - 默认 + 默认 空闲 已使用 应用 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 23b1a7ed..9e0575da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,11 +77,11 @@ %d %.1f/10.0 %d - %s Ep %d + %1$s Ep %2$d Cast: %s Episode %d will be released in - %dd %dh %dm - %dh %dm + %1$dd %2$dh %3$dm + %1$dh %2$dm %dm Poster @@ -97,7 +97,7 @@ Speed (%.2fx) Rated: %.1f - New update found!\n%s -> %s + New update found!\n%1$s -> %2$s Filler %d min CloudStream @@ -123,7 +123,7 @@ Completed Dropped Plan to Watch - None + @string/none Rewatching Play Movie Play Trailer @@ -164,7 +164,7 @@ Remove Set watch status Apply - Cancel + @string/cancel Copy Close Clear @@ -276,18 +276,18 @@ developers Season - %s %d%s + %1$s %2$d%3$s No Season Episode Episodes - %d-%d - %d %s + %1$d-%2$d + %1$d %2$s S E No Episodes found Delete File Delete - @string/sort_cancel + Cancel Pause Start Failed @@ -307,8 +307,9 @@ Synopsis queued No Subtitles - Default - @string/default_subtitles + Default + @string/action_default + @string/action_default Free Used App @@ -470,7 +471,7 @@ Kitsu Trakt --> - %s %s + %1$s %2$s account Log out Log in @@ -571,8 +572,8 @@ Plugin Deleted Could not load %s 18+ - Started downloading %d %s… - Downloaded %d %s + Started downloading %1$d %2$s… + Downloaded %1$d %2$s All %s already downloaded No plugins found in repository Repository not found, check the URL and try VPN @@ -666,6 +667,8 @@ Subscribed to %s Unsubscribed from %s Episode %d released! + Subscribe + Unsubscribe Profile %d Wi-Fi Mobile data @@ -686,13 +689,41 @@ Qualities Profile background UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s - - - tv_no_focus_tag You have already voted + Favorites %s added to favorites %s removed from favorites Add to favorites Remove from favorites + + Potential Duplicate Found + Add + Replace + Replace All + @string/cancel + + It appears that a potentially duplicate item already exists in your library: \'%s.\' + + \n\nWould you like to add this item anyway, replace the existing one, or cancel the action? + + + Potential duplicate items have been found in your library: + + \n\n%s + + \n\nWould you like to add this item anyway, replace the existing ones, or cancel the action? + + + + tv_no_focus_tag + + + Enter PIN + Enter Current PIN + Lock Profile + PIN + Incorrect PIN. Please try again. + PIN must be 4 characters + Select an Account diff --git a/fastlane/metadata/android/es-ES/changelogs/2.txt b/fastlane/metadata/android/es-ES/changelogs/2.txt new file mode 100644 index 00000000..68e0b99b --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/2.txt @@ -0,0 +1 @@ +- ¡Cambios añadidos! diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt new file mode 100644 index 00000000..9f6accba --- /dev/null +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 te permite ver y descargar películas, series de TV y anime. + +La aplicación viene sin ningún tipo de anuncios y análisis y +soporta múltiples tráilers y páginas de películas, y más, por ejemplo + +Marcadores + +Descargas de subtítulos + +Compatible con Chromecast diff --git a/fastlane/metadata/android/es-ES/short_description.txt b/fastlane/metadata/android/es-ES/short_description.txt new file mode 100644 index 00000000..eb1d11e8 --- /dev/null +++ b/fastlane/metadata/android/es-ES/short_description.txt @@ -0,0 +1 @@ +Vea y descargue películas, series de televisión y anime. diff --git a/fastlane/metadata/android/es-ES/title.txt b/fastlane/metadata/android/es-ES/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/es-ES/title.txt @@ -0,0 +1 @@ +CloudStream