From ab44372e1aafd5a3a12e4a72fd88846e73614819 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 2 Dec 2025 21:58:38 -0500 Subject: [PATCH] Full size dialog --- .../java/io/heckel/ntfy/ui/AddFragment.kt | 146 ++-- app/src/main/res/anim/slide_in_bottom.xml | 9 + app/src/main/res/anim/slide_out_bottom.xml | 9 + .../main/res/drawable/ic_close_white_24dp.xml | 11 + .../main/res/layout/fragment_add_dialog.xml | 625 +++++++++--------- app/src/main/res/menu/menu_add_dialog.xml | 10 + app/src/main/res/values/themes.xml | 15 + 7 files changed, 435 insertions(+), 390 deletions(-) create mode 100644 app/src/main/res/anim/slide_in_bottom.xml create mode 100644 app/src/main/res/anim/slide_out_bottom.xml create mode 100644 app/src/main/res/drawable/ic_close_white_24dp.xml create mode 100644 app/src/main/res/menu/menu_add_dialog.xml diff --git a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt index 0497705d..26772075 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt @@ -1,17 +1,18 @@ package io.heckel.ntfy.ui import android.app.Activity -import android.app.AlertDialog import android.app.Dialog import android.content.Context import android.os.Bundle +import android.view.MenuItem import android.view.View +import android.view.ViewGroup import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.* import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import io.heckel.ntfy.BuildConfig @@ -31,10 +32,10 @@ class AddFragment : DialogFragment() { private lateinit var appBaseUrl: String private var defaultBaseUrl: String? = null + private lateinit var toolbar: MaterialToolbar + private lateinit var actionMenuItem: MenuItem private lateinit var subscribeView: View private lateinit var loginView: View - private lateinit var positiveButton: Button - private lateinit var negativeButton: Button // Subscribe page private lateinit var subscribeTopicText: TextInputEditText @@ -79,6 +80,21 @@ class AddFragment : DialogFragment() { // Build root view val view = requireActivity().layoutInflater.inflate(R.layout.fragment_add_dialog, null) + // Setup toolbar + toolbar = view.findViewById(R.id.add_dialog_toolbar) + toolbar.setNavigationOnClickListener { + dismiss() + } + toolbar.setOnMenuItemClickListener { menuItem -> + if (menuItem.itemId == R.id.add_dialog_action_button) { + onActionButtonClick() + true + } else { + false + } + } + actionMenuItem = toolbar.menu.findItem(R.id.add_dialog_action_button) + // Main "pages" subscribeView = view.findViewById(R.id.add_dialog_subscribe_view) subscribeView.visibility = View.VISIBLE @@ -137,6 +153,19 @@ class AddFragment : DialogFragment() { } } + // Subscribe view validation + val subscribeTextWatcher = AfterChangedTextWatcher { + validateInputSubscribeView() + } + subscribeTopicText.addTextChangedListener(subscribeTextWatcher) + subscribeBaseUrlText.addTextChangedListener(subscribeTextWatcher) + subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, _ -> + validateInputSubscribeView() + } + subscribeUseAnotherServerCheckbox.setOnCheckedChangeListener { _, _ -> + validateInputSubscribeView() + } + // Username/password validation on type val loginTextWatcher = AfterChangedTextWatcher { validateInputLoginView() @@ -145,57 +174,36 @@ class AddFragment : DialogFragment() { loginPasswordText.addTextChangedListener(loginTextWatcher) // Build dialog - // Build fullscreen dialog - val dialog = Dialog(requireContext(), android.R.style.Theme_Material_Light_NoActionBar_Fullscreen) + val dialog = Dialog(requireContext(), R.style.Theme_App_FullScreenDialog) dialog.setContentView(view) - dialog.setCanceledOnTouchOutside(false) - // Force fullscreen size - dialog.window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT ) - - // Add your buttons manually since we no longer use a builder - positiveButton = view.findViewById(R.id.add_dialog_positive_button) - ?: Button(requireContext()).also { - // If you don't have dedicated buttons in layout, create them dynamically - } - - negativeButton = view.findViewById(R.id.add_dialog_negative_button) - ?: Button(requireContext()) - - - // Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785) - dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) - - // Add logic to disable "Subscribe" button on invalid input - dialog.setOnShowListener { - positiveButton.isEnabled = false - positiveButton.setOnClickListener { - positiveButtonClick() - } - negativeButton.setOnClickListener { - negativeButtonClick() - } - val subscribeTextWatcher = AfterChangedTextWatcher { - validateInputSubscribeView() - } - subscribeTopicText.addTextChangedListener(subscribeTextWatcher) - subscribeBaseUrlText.addTextChangedListener(subscribeTextWatcher) - subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, _ -> - validateInputSubscribeView() - } - subscribeUseAnotherServerCheckbox.setOnCheckedChangeListener { _, _ -> - validateInputSubscribeView() - } - validateInputSubscribeView() - - // Focus topic text (keyboard is shown too, see above) - subscribeTopicText.requestFocus() - } + // Initial validation + validateInputSubscribeView() return dialog } - private fun positiveButtonClick() { + override fun onStart() { + super.onStart() + dialog?.window?.apply { + setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + } + + override fun onResume() { + super.onResume() + // Show keyboard after the dialog is fully visible + subscribeTopicText.postDelayed({ + subscribeTopicText.requestFocus() + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(subscribeTopicText, InputMethodManager.SHOW_FORCED) + }, 200) + } + + private fun onActionButtonClick() { val topic = subscribeTopicText.text.toString() val baseUrl = getBaseUrl() if (subscribeView.visibility == View.VISIBLE) { @@ -287,16 +295,8 @@ class AddFragment : DialogFragment() { } } - private fun negativeButtonClick() { - if (subscribeView.visibility == View.VISIBLE) { - dialog?.cancel() - } else if (loginView.visibility == View.VISIBLE) { - showSubscribeView() - } - } - private fun validateInputSubscribeView() { - if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play + if (!this::actionMenuItem.isInitialized) return // As per crash seen in Google Play // Show/hide things: This logic is intentionally kept simple. Do not simplify "just because it's pretty". val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) { @@ -334,11 +334,11 @@ class AddFragment : DialogFragment() { activity?.let { it.runOnUiThread { if (subscription != null || DISALLOWED_TOPICS.contains(topic)) { - positiveButton.isEnabled = false + actionMenuItem.isEnabled = false } else if (subscribeUseAnotherServerCheckbox.isChecked) { - positiveButton.isEnabled = validTopic(topic) && validUrl(baseUrl) + actionMenuItem.isEnabled = validTopic(topic) && validUrl(baseUrl) } else { - positiveButton.isEnabled = validTopic(topic) + actionMenuItem.isEnabled = validTopic(topic) } } } @@ -346,13 +346,13 @@ class AddFragment : DialogFragment() { } private fun validateInputLoginView() { - if (!this::positiveButton.isInitialized || !this::loginUsernameText.isInitialized || !this::loginPasswordText.isInitialized) { + if (!this::actionMenuItem.isInitialized || !this::loginUsernameText.isInitialized || !this::loginPasswordText.isInitialized) { return // As per crash seen in Google Play } if (loginUsernameText.visibility == View.GONE) { - positiveButton.isEnabled = true + actionMenuItem.isEnabled = true } else { - positiveButton.isEnabled = (loginUsernameText.text?.isNotEmpty() ?: false) + actionMenuItem.isEnabled = (loginUsernameText.text?.isNotEmpty() ?: false) && (loginPasswordText.text?.isNotEmpty() ?: false) } } @@ -379,8 +379,11 @@ class AddFragment : DialogFragment() { private fun showSubscribeView() { resetSubscribeView() - positiveButton.text = getString(R.string.add_dialog_button_subscribe) - negativeButton.text = getString(R.string.add_dialog_button_cancel) + toolbar.setTitle(R.string.add_dialog_title) + actionMenuItem.setTitle(R.string.add_dialog_button_subscribe) + toolbar.setNavigationOnClickListener { + dismiss() + } loginView.visibility = View.GONE subscribeView.visibility = View.VISIBLE if (subscribeTopicText.requestFocus()) { @@ -392,8 +395,11 @@ class AddFragment : DialogFragment() { private fun showLoginView(activity: Activity) { resetLoginView() loginProgress.visibility = View.INVISIBLE - positiveButton.text = getString(R.string.add_dialog_button_login) - negativeButton.text = getString(R.string.add_dialog_button_back) + toolbar.setTitle(R.string.add_dialog_login_title) + actionMenuItem.setTitle(R.string.add_dialog_button_login) + toolbar.setNavigationOnClickListener { + showSubscribeView() + } subscribeView.visibility = View.GONE loginView.visibility = View.VISIBLE if (loginUsernameText.requestFocus()) { @@ -407,7 +413,7 @@ class AddFragment : DialogFragment() { subscribeBaseUrlText.isEnabled = enable subscribeInstantDeliveryCheckbox.isEnabled = enable subscribeUseAnotherServerCheckbox.isEnabled = enable - positiveButton.isEnabled = enable + actionMenuItem.isEnabled = enable } private fun resetSubscribeView() { @@ -420,7 +426,7 @@ class AddFragment : DialogFragment() { private fun enableLoginView(enable: Boolean) { loginUsernameText.isEnabled = enable loginPasswordText.isEnabled = enable - positiveButton.isEnabled = enable + actionMenuItem.isEnabled = enable if (enable && loginUsernameText.requestFocus()) { val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT) diff --git a/app/src/main/res/anim/slide_in_bottom.xml b/app/src/main/res/anim/slide_in_bottom.xml new file mode 100644 index 00000000..dc163787 --- /dev/null +++ b/app/src/main/res/anim/slide_in_bottom.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/anim/slide_out_bottom.xml b/app/src/main/res/anim/slide_out_bottom.xml new file mode 100644 index 00000000..362a7564 --- /dev/null +++ b/app/src/main/res/anim/slide_out_bottom.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_close_white_24dp.xml b/app/src/main/res/drawable/ic_close_white_24dp.xml new file mode 100644 index 00000000..9ca62566 --- /dev/null +++ b/app/src/main/res/drawable/ic_close_white_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_add_dialog.xml b/app/src/main/res/layout/fragment_add_dialog.xml index 8ce8874d..910d1678 100644 --- a/app/src/main/res/layout/fragment_add_dialog.xml +++ b/app/src/main/res/layout/fragment_add_dialog.xml @@ -1,355 +1,340 @@ - - + + + android:background="?attr/colorSurface" + app:elevation="0dp" + app:liftOnScroll="false"> -