package com.lagradost.cloudstream3.ui.account import android.app.Activity import android.content.Context import android.content.DialogInterface import android.content.Intent import android.text.Editable 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.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.AccountEditDialogBinding import com.lagradost.cloudstream3.databinding.AccountSelectLinearBinding import com.lagradost.cloudstream3.databinding.LockPinDialogBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod object AccountHelper { fun showAccountEditDialog( context: Context, account: DataStoreHelper.Account, isNewAccount: Boolean, accountEditCallback: (DataStoreHelper.Account) -> Unit, accountDeleteCallback: (DataStoreHelper.Account) -> Unit ) { val binding = AccountEditDialogBinding.inflate(LayoutInflater.from(context), null, false) val builder = AlertDialog.Builder(context, R.style.AlertDialogCustom) .setView(binding.root) var currentEditAccount = account val dialog = builder.show() if (!isNewAccount) binding.title.setText(R.string.edit_account) // Set up the dialog content binding.accountName.text = Editable.Factory.getInstance()?.newEditable(account.name) binding.accountName.doOnTextChanged { text, _, _, _ -> currentEditAccount = currentEditAccount.copy(name = text?.toString() ?: "") } binding.deleteBtt.isGone = isNewAccount binding.deleteBtt.setOnClickListener { val dialogClickListener = DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { accountDeleteCallback.invoke(account) dialog?.dismissSafe() } DialogInterface.BUTTON_NEGATIVE -> { dialog?.dismissSafe() } } } try { AlertDialog.Builder(context).setTitle(R.string.delete).setMessage( context.getString(R.string.delete_message).format( currentEditAccount.name ) ) .setPositiveButton(R.string.delete, dialogClickListener) .setNegativeButton(R.string.cancel, dialogClickListener) .show().setDefaultFocus() } catch (t: Throwable) { logError(t) } } binding.cancelBtt.setOnClickListener { dialog?.dismissSafe() } // Handle the profile picture and its interactions binding.accountImage.setImage(account.image) binding.accountImage.setOnClickListener { // Roll the image forwards once currentEditAccount = currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % DataStoreHelper.profileImages.size) binding.accountImage.setImage(currentEditAccount.image) } // Handle applying changes binding.applyBtt.setOnClickListener { 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 accountEditCallback.invoke(currentEditAccount) dialog.dismissSafe() } } else { // No lock PIN set, proceed to update the account accountEditCallback.invoke(currentEditAccount) 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 } fun showPinInputDialog( context: Context, currentPin: String?, editAccount: Boolean, forStartup: Boolean = false, 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 (forStartup) { val currentAccount = DataStoreHelper.accounts.firstOrNull { it.keyIndex == DataStoreHelper.selectedKeyIndex } builder.setTitle(context.getString(R.string.enter_pin_with_name, currentAccount?.name)) builder.setOnDismissListener { if (!isPinValid) { context.getActivity()?.finish() } } // So that if they don't know the PIN for the current account, // they don't get completely locked out builder.setNeutralButton(R.string.use_default_account) { _, _ -> val activity = context.getActivity() if (activity is AccountSelectActivity) { isPinValid = true activity.viewModel.handleAccountSelect(getDefaultAccount(context), activity) } } } 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) } fun Activity?.showAccountSelectLinear() { val activity = this as? MainActivity ?: return val viewModel = ViewModelProvider(activity)[AccountViewModel::class.java] val binding: AccountSelectLinearBinding = AccountSelectLinearBinding.inflate( LayoutInflater.from(activity) ) val builder = BottomSheetDialog(activity) builder.setContentView(binding.root) builder.show() binding.manageAccountsButton.setOnClickListener { val accountSelectIntent = Intent(activity, AccountSelectActivity::class.java) accountSelectIntent.putExtra("isEditingFromMainActivity", true) activity.startActivity(accountSelectIntent) builder.dismissSafe() } val recyclerView: RecyclerView = binding.accountRecyclerView val itemSize = recyclerView.resources.getDimensionPixelSize( R.dimen.account_select_linear_item_size ) recyclerView.addItemDecoration(AccountSelectLinearItemDecoration(itemSize)) recyclerView.setLinearListLayout(isHorizontal = true) val currentAccount = DataStoreHelper.accounts.firstOrNull { it.keyIndex == DataStoreHelper.selectedKeyIndex } ?: getDefaultAccount(activity) // We want to make sure the accounts are up-to-date viewModel.handleAccountSelect( currentAccount, activity, reloadForActivity = true ) activity.observe(viewModel.accounts) { liveAccounts -> recyclerView.adapter = AccountAdapter( liveAccounts, accountSelectCallback = { account -> viewModel.handleAccountSelect(account, activity) builder.dismissSafe() }, accountCreateCallback = { viewModel.handleAccountUpdate(it, activity) }, accountEditCallback = { viewModel.handleAccountUpdate(it, activity) }, accountDeleteCallback = { viewModel.handleAccountDelete(it, activity) } ) activity.observe(viewModel.selectedKeyIndex) { selectedKeyIndex -> // Scroll to current account (which is focused by default) val layoutManager = recyclerView.layoutManager as LinearLayoutManager layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0) } } } }