package com.futo.platformplayer import android.app.AlertDialog import android.content.Context import android.graphics.Color import android.util.TypedValue import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.* import androidx.core.content.ContextCompat import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.dialogs.* import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.stores.v2.ManagedStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import userpackage.Protocol import java.io.File class UIDialogs { companion object { private val TAG = "Dialogs" private val _openDialogs = arrayListOf<AlertDialog>(); private fun registerDialogOpened(dialog: AlertDialog) { _openDialogs.add(dialog); } private fun registerDialogClosed(dialog: AlertDialog) { _openDialogs.remove(dialog); } fun dismissAllDialogs() { for (openDialog in _openDialogs) { openDialog.dismiss(); } _openDialogs.clear(); } fun showDialogProgress(context: Context, handler: ((ProgressDialog)->Unit)) { val dialog = ProgressDialog(context, handler); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); } fun showDialogOk(context: Context, icon: Int, text: String, handler: (()->Unit)? = null) { showDialog(context, icon, text, null, null, 0, Action("Ok", { handler?.invoke(); }, ActionStyle.PRIMARY)); } fun multiShowDialog(context: Context, finally: (() -> Unit)?, vararg dialogDescriptor: Descriptor?) = multiShowDialog(context, dialogDescriptor.toList(), finally); fun multiShowDialog(context: Context, vararg dialogDescriptor: Descriptor?) = multiShowDialog(context, dialogDescriptor.toList()); fun multiShowDialog(context: Context, dialogDescriptor: List<Descriptor?>, finally: (()->Unit)? = null) { if(dialogDescriptor.isEmpty()) { if (finally != null) { finally() }; return; } if(dialogDescriptor[0] == null) { multiShowDialog(context, dialogDescriptor.drop(1), finally); return; } val currentDialog = dialogDescriptor[0]!!; if(!currentDialog.shouldShow()) { multiShowDialog(context, dialogDescriptor.drop(1), finally); return; } showDialog(context, currentDialog.icon, currentDialog.text, currentDialog.textDetails, currentDialog.code, currentDialog.defaultCloseAction, *currentDialog.actions.map { return@map Action(it.text, { it.action(); multiShowDialog(context, dialogDescriptor.drop(1), finally); }, it.style); }.toTypedArray()); } fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) { val dialogAction: ()->Unit = { val dialog = AutomaticBackupDialog(context); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog); onClosed?.invoke() }; dialog.show(); }; if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck) UIDialogs.showDialog(context, R.drawable.ic_move_up, "An old backup is available", "Would you like to restore this backup?", null, 0, UIDialogs.Action("Cancel", {}), //To nothing UIDialogs.Action("Override", { dialogAction(); }, UIDialogs.ActionStyle.DANGEROUS), UIDialogs.Action("Restore", { UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope); }, UIDialogs.ActionStyle.PRIMARY)); else { dialogAction(); } } fun showAutomaticRestoreDialog(context: Context, scope: CoroutineScope) { val dialog = AutomaticRestoreDialog(context, scope); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); } fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) { val builder = AlertDialog.Builder(context); val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null); builder.setView(view); val dialog = builder.create(); registerDialogOpened(dialog); view.findViewById<ImageView>(R.id.dialog_icon).apply { this.setImageResource(icon); } view.findViewById<TextView>(R.id.dialog_text).apply { this.text = text; }; view.findViewById<TextView>(R.id.dialog_text_details).apply { if(textDetails == null) this.visibility = View.GONE; else this.text = textDetails; }; view.findViewById<TextView>(R.id.dialog_text_code).apply { if(code == null) this.visibility = View.GONE; else this.text = code; }; view.findViewById<LinearLayout>(R.id.dialog_buttons).apply { val buttons = actions.map<Action, TextView> { act -> val buttonView = TextView(context); val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt(); val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt(); val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt(); buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { if(actions.size > 1) this.marginEnd = if(actions.size > 2) dp14 else dp28; }; buttonView.setTextColor(Color.WHITE); buttonView.textSize = 14f; buttonView.typeface = resources.getFont(R.font.inter_regular); buttonView.text = act.text; buttonView.setOnClickListener { act.action(); dialog.dismiss(); }; when(act.style) { ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary); ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent); ActionStyle.DANGEROUS -> buttonView.setBackgroundResource(R.drawable.background_button_pred); ActionStyle.DANGEROUS_TEXT -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.pastel_red)) else -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary)) } val paddingSpecialButtons = if(actions.size > 2) dp14 else dp28; if(act.style != ActionStyle.NONE && act.style != ActionStyle.DANGEROUS_TEXT) buttonView.setPadding(paddingSpecialButtons, dp10, paddingSpecialButtons, dp10); else buttonView.setPadding(dp10, dp10, dp10, dp10); return@map buttonView; }; if(actions.size <= 1) this.gravity = Gravity.CENTER; else this.gravity = Gravity.END; for(button in buttons) this.addView(button); }; dialog.setOnCancelListener { if(defaultCloseAction >= 0 && defaultCloseAction < actions.size) actions[defaultCloseAction].action(); } dialog.setOnDismissListener { registerDialogClosed(dialog); } dialog.show(); } fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) { showDialog(context, R.drawable.ic_error_pred, msg, (if(ex != null ) "${ex.message}" else ""), if(ex is PluginException) ex.code else null, 0, UIDialogs.Action(button, { onOk?.invoke(); }, UIDialogs.ActionStyle.PRIMARY) ); } fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) { val pluginInfo = if(ex is PluginException) "\nPlugin [${ex.config.name}]" else ""; showDialog(context, R.drawable.ic_error_pred, "${msg}${pluginInfo}", (if(ex != null ) "${ex.message}" else ""), if(ex is PluginException) ex.code else null, 0, UIDialogs.Action("Retry", { retryAction?.invoke(); }, UIDialogs.ActionStyle.PRIMARY), UIDialogs.Action("Close", { closeAction?.invoke() }, UIDialogs.ActionStyle.NONE) ); } fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) { val singleButtonAction = Action(buttonText, action) showDialog(context, icon, text, null, null, -1, singleButtonAction) } fun showDataRetryDialog(context: Context, reason: String? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) { val retryButtonAction = Action("Retry", retryAction ?: {}, ActionStyle.PRIMARY) val closeButtonAction = Action("Close", closeAction ?: {}, ActionStyle.ACCENT) showDialog(context, R.drawable.ic_no_internet_86dp, "Data Retry", reason, null, 0, closeButtonAction, retryButtonAction) } fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) { val confirmButtonAction = Action("Confirm", action, ActionStyle.PRIMARY) val cancelButtonAction = Action("Cancel", cancelAction ?: {}, ActionStyle.ACCENT) showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction) } fun showUpdateAvailableDialog(context: Context, lastVersion: Int) { val dialog = AutoUpdateDialog(context); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); dialog.setMaxVersion(lastVersion); } fun showChangelogDialog(context: Context, lastVersion: Int) { val dialog = ChangelogDialog(context); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); dialog.setMaxVersion(lastVersion); } fun showInstallDownloadedUpdateDialog(context: Context, apkFile: File) { val dialog = AutoUpdateDialog(context); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.showPredownloaded(apkFile); } fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) { if(!store.hasMissingReconstructions()) onConcluded(); else { val dialog = MigrateDialog(context, store, onConcluded); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); } } fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) { val dialog = ImportDialog(context, store, name, reconstructions, onConcluded); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); } fun showCastingDialog(context: Context) { val d = StateCasting.instance.activeDevice; if (d != null) { val dialog = ConnectedCastingDialog(context); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); } else { val dialog = ConnectCastingDialog(context); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); } } fun showCastingAddDialog(context: Context) { val dialog = CastingAddDialog(context); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.show(); } fun toast(context : Context, text : String, long : Boolean = false) { Toast.makeText(context, text, if(long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show(); } fun toast(text : String, long : Boolean = false) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { try { StateApp.withContext { Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show(); } } catch (e: Throwable) { Logger.e(TAG, "Failed to show toast.", e); } } } fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) { //TODO: Is not actually clickable... val toastDuration = if (isLongDuration) Toast.LENGTH_LONG else Toast.LENGTH_SHORT val toast = Toast(context) val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val customView = inflater.inflate(R.layout.toast_clickable, null) val toastTextView: TextView = customView.findViewById(R.id.toast_text) toastTextView.text = text customView.setOnClickListener { onClick() } toast.view = customView toast.duration = toastDuration toast.show() } fun showCommentDialog(context: Context, contextUrl: String, ref: Protocol.Reference, onCommentAdded: (comment: IPlatformComment) -> Unit) { val dialog = CommentDialog(context, contextUrl, ref); registerDialogOpened(dialog); dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.onCommentAdded.subscribe { onCommentAdded(it); }; dialog.show() } } class Descriptor(val icon: Int, val text: String, val textDetails: String? = null, val code: String? = null, val defaultCloseAction: Int, vararg acts: Action) { var shouldShow: ()->Boolean = {true}; val actions: List<Action> = acts.toList(); fun withCondition(shouldShow: () -> Boolean): Descriptor { this.shouldShow = shouldShow; return this; } } class Action { val text: String; val action: ()->Unit; val style: ActionStyle; constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE) { this.text = text; this.action = action; this.style = style; } } enum class ActionStyle { NONE, PRIMARY, ACCENT, DANGEROUS, DANGEROUS_TEXT } }