From e7ecdc266e83dbe69fe6ab4cbd3e3e653baa1999 Mon Sep 17 00:00:00 2001 From: Philipp Heckel <pheckel@datto.com> Date: Fri, 28 Jan 2022 00:02:20 -0500 Subject: [PATCH] Continued dialogs --- .../java/io/heckel/ntfy/ui/AddFragment.kt | 248 ++++++++++-------- .../main/res/drawable/ic_error_black_24dp.xml | 9 + .../main/res/layout/fragment_add_dialog.xml | 113 +++++--- app/src/main/res/values/strings.xml | 10 +- assets/error_black_24dp.svg | 1 + 5 files changed, 231 insertions(+), 150 deletions(-) create mode 100644 app/src/main/res/drawable/ic_error_black_24dp.xml create mode 100644 assets/error_black_24dp.svg 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 85783b2..b08829c 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt @@ -19,7 +19,6 @@ import io.heckel.ntfy.db.User import io.heckel.ntfy.log.Log import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.util.topicUrl -import kotlinx.android.synthetic.main.fragment_add_dialog.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.random.Random @@ -34,23 +33,25 @@ class AddFragment : DialogFragment() { private lateinit var loginView: View // Subscribe page - private lateinit var topicNameText: TextInputEditText - private lateinit var baseUrlLayout: TextInputLayout - private lateinit var baseUrlText: AutoCompleteTextView - private lateinit var useAnotherServerCheckbox: CheckBox - private lateinit var useAnotherServerDescription: TextView - private lateinit var instantDeliveryBox: View - private lateinit var instantDeliveryCheckbox: CheckBox - private lateinit var instantDeliveryDescription: View + private lateinit var subscribeTopicText: TextInputEditText + private lateinit var subscribeBaseUrlLayout: TextInputLayout + private lateinit var subscribeBaseUrlText: AutoCompleteTextView + private lateinit var subscribeUseAnotherServerCheckbox: CheckBox + private lateinit var subscribeUseAnotherServerDescription: TextView + private lateinit var subscribeInstantDeliveryBox: View + private lateinit var subscribeInstantDeliveryCheckbox: CheckBox + private lateinit var subscribeInstantDeliveryDescription: View + private lateinit var subscribeProgress: ProgressBar + private lateinit var subscribeErrorImage: View private lateinit var subscribeButton: Button // Login page private lateinit var users: List<User> - private lateinit var usersSpinner: Spinner - private lateinit var usernameText: TextInputEditText - private lateinit var passwordText: TextInputEditText + private lateinit var loginUsersSpinner: Spinner + private lateinit var loginUsernameText: TextInputEditText + private lateinit var loginPasswordText: TextInputEditText private lateinit var loginProgress: ProgressBar - private lateinit var loginError: TextView + private lateinit var loginErrorImage: View private lateinit var baseUrls: List<String> // List of base URLs already used, excluding app_base_url @@ -80,24 +81,26 @@ class AddFragment : DialogFragment() { loginView.visibility = View.GONE // Fields for "subscribe page" - topicNameText = view.findViewById(R.id.add_dialog_topic_text) - baseUrlLayout = view.findViewById(R.id.add_dialog_base_url_layout) - baseUrlText = view.findViewById(R.id.add_dialog_base_url_text) - instantDeliveryBox = view.findViewById(R.id.add_dialog_instant_delivery_box) - instantDeliveryCheckbox = view.findViewById(R.id.add_dialog_instant_delivery_checkbox) - instantDeliveryDescription = view.findViewById(R.id.add_dialog_instant_delivery_description) - useAnotherServerCheckbox = view.findViewById(R.id.add_dialog_use_another_server_checkbox) - useAnotherServerDescription = view.findViewById(R.id.add_dialog_use_another_server_description) + subscribeTopicText = view.findViewById(R.id.add_dialog_topic_text) + subscribeBaseUrlLayout = view.findViewById(R.id.add_dialog_base_url_layout) + subscribeBaseUrlText = view.findViewById(R.id.add_dialog_base_url_text) + subscribeInstantDeliveryBox = view.findViewById(R.id.add_dialog_instant_delivery_box) + subscribeInstantDeliveryCheckbox = view.findViewById(R.id.add_dialog_instant_delivery_checkbox) + subscribeInstantDeliveryDescription = view.findViewById(R.id.add_dialog_instant_delivery_description) + subscribeUseAnotherServerCheckbox = view.findViewById(R.id.add_dialog_use_another_server_checkbox) + subscribeUseAnotherServerDescription = view.findViewById(R.id.add_dialog_use_another_server_description) + subscribeProgress = view.findViewById(R.id.add_dialog_progress) + subscribeErrorImage = view.findViewById(R.id.add_dialog_error_image) // Fields for "login page" - usersSpinner = view.findViewById(R.id.add_dialog_login_users_spinner) - usernameText = view.findViewById(R.id.add_dialog_login_username) - passwordText = view.findViewById(R.id.add_dialog_login_password) + loginUsersSpinner = view.findViewById(R.id.add_dialog_login_users_spinner) + loginUsernameText = view.findViewById(R.id.add_dialog_login_username) + loginPasswordText = view.findViewById(R.id.add_dialog_login_password) loginProgress = view.findViewById(R.id.add_dialog_login_progress) - loginError = view.findViewById(R.id.add_dialog_login_error) + loginErrorImage = view.findViewById(R.id.add_dialog_login_error_image) // Set "Use another server" description based on flavor - useAnotherServerDescription.text = if (BuildConfig.FIREBASE_AVAILABLE) { + subscribeUseAnotherServerDescription.text = if (BuildConfig.FIREBASE_AVAILABLE) { getString(R.string.add_dialog_use_another_server_description) } else { getString(R.string.add_dialog_use_another_server_description_noinstant) @@ -105,29 +108,29 @@ class AddFragment : DialogFragment() { // Base URL dropdown behavior; Oh my, why is this so complicated?! val toggleEndIcon = { - if (baseUrlText.text.isNotEmpty()) { - baseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp) + if (subscribeBaseUrlText.text.isNotEmpty()) { + subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp) } else if (baseUrls.isEmpty()) { - baseUrlLayout.setEndIconDrawable(0) + subscribeBaseUrlLayout.setEndIconDrawable(0) } else { - baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp) + subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp) } } - baseUrlLayout.setEndIconOnClickListener { - if (baseUrlText.text.isNotEmpty()) { - baseUrlText.text.clear() + subscribeBaseUrlLayout.setEndIconOnClickListener { + if (subscribeBaseUrlText.text.isNotEmpty()) { + subscribeBaseUrlText.text.clear() if (baseUrls.isEmpty()) { - baseUrlLayout.setEndIconDrawable(0) + subscribeBaseUrlLayout.setEndIconDrawable(0) } else { - baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp) + subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp) } - } else if (baseUrlText.text.isEmpty() && baseUrls.isNotEmpty()) { - baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_up_gray_24dp) - baseUrlText.showDropDown() + } else if (subscribeBaseUrlText.text.isEmpty() && baseUrls.isNotEmpty()) { + subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_up_gray_24dp) + subscribeBaseUrlText.showDropDown() } } - baseUrlText.setOnDismissListener { toggleEndIcon() } - baseUrlText.addTextChangedListener(object : TextWatcher { + subscribeBaseUrlText.setOnDismissListener { toggleEndIcon() } + subscribeBaseUrlText.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable?) { toggleEndIcon() } @@ -150,41 +153,34 @@ class AddFragment : DialogFragment() { .sorted() val adapter = ArrayAdapter(requireActivity(), R.layout.fragment_add_dialog_dropdown_item, baseUrls) requireActivity().runOnUiThread { - baseUrlText.threshold = 1 - baseUrlText.setAdapter(adapter) + subscribeBaseUrlText.threshold = 1 + subscribeBaseUrlText.setAdapter(adapter) if (baseUrls.count() == 1) { - baseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp) - baseUrlText.setText(baseUrls.first()) + subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp) + subscribeBaseUrlText.setText(baseUrls.first()) } else if (baseUrls.count() > 1) { - baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp) + subscribeBaseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp) } else { - baseUrlLayout.setEndIconDrawable(0) + subscribeBaseUrlLayout.setEndIconDrawable(0) } } // Users dropdown users = repository.getUsers() - if (users.isEmpty()) { - usersSpinner.visibility = View.GONE - } else { - val spinnerEntries = users.toMutableList() - spinnerEntries.add(0, User(0, "Create new", "")) // FIXME - usersSpinner.adapter = ArrayAdapter(requireActivity(), R.layout.fragment_add_dialog_dropdown_item, spinnerEntries) - } } // Show/hide based on flavor - instantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE + subscribeInstantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE // Show/hide spinner and username/password fields - usersSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + loginUsersSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { if (position == 0) { - usernameText.visibility = View.VISIBLE - passwordText.visibility = View.VISIBLE + loginUsernameText.visibility = View.VISIBLE + loginPasswordText.visibility = View.VISIBLE } else { - usernameText.visibility = View.GONE - passwordText.visibility = View.GONE + loginUsernameText.visibility = View.GONE + loginPasswordText.visibility = View.GONE } } override fun onNothingSelected(parent: AdapterView<*>?) { @@ -224,24 +220,24 @@ class AddFragment : DialogFragment() { // Nothing } } - topicNameText.addTextChangedListener(textWatcher) - baseUrlText.addTextChangedListener(textWatcher) - instantDeliveryCheckbox.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) instantDeliveryDescription.visibility = View.VISIBLE - else instantDeliveryDescription.visibility = View.GONE + subscribeTopicText.addTextChangedListener(textWatcher) + subscribeBaseUrlText.addTextChangedListener(textWatcher) + subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) subscribeInstantDeliveryDescription.visibility = View.VISIBLE + else subscribeInstantDeliveryDescription.visibility = View.GONE } - useAnotherServerCheckbox.setOnCheckedChangeListener { _, isChecked -> + subscribeUseAnotherServerCheckbox.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - useAnotherServerDescription.visibility = View.VISIBLE - baseUrlLayout.visibility = View.VISIBLE - instantDeliveryBox.visibility = View.GONE - instantDeliveryDescription.visibility = View.GONE + subscribeUseAnotherServerDescription.visibility = View.VISIBLE + subscribeBaseUrlLayout.visibility = View.VISIBLE + subscribeInstantDeliveryBox.visibility = View.GONE + subscribeInstantDeliveryDescription.visibility = View.GONE } else { - useAnotherServerDescription.visibility = View.GONE - baseUrlLayout.visibility = View.GONE - instantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE - if (instantDeliveryCheckbox.isChecked) instantDeliveryDescription.visibility = View.VISIBLE - else instantDeliveryDescription.visibility = View.GONE + subscribeUseAnotherServerDescription.visibility = View.GONE + subscribeBaseUrlLayout.visibility = View.GONE + subscribeInstantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE + if (subscribeInstantDeliveryCheckbox.isChecked) subscribeInstantDeliveryDescription.visibility = View.VISIBLE + else subscribeInstantDeliveryDescription.visibility = View.GONE } validateInput() } @@ -251,7 +247,7 @@ class AddFragment : DialogFragment() { } private fun subscribeButtonClick() { - val topic = topicNameText.text.toString() + val topic = subscribeTopicText.text.toString() val baseUrl = getBaseUrl() if (subscribeView.visibility == View.VISIBLE) { checkAnonReadAndMaybeShowLogin(baseUrl, topic) @@ -261,19 +257,44 @@ class AddFragment : DialogFragment() { } private fun checkAnonReadAndMaybeShowLogin(baseUrl: String, topic: String) { + subscribeProgress.visibility = View.VISIBLE + subscribeErrorImage.visibility = View.GONE lifecycleScope.launch(Dispatchers.IO) { Log.d(TAG, "Checking anonymous read access to topic ${topicUrl(baseUrl, topic)}") - val authorized = api.checkAnonTopicRead(baseUrl, topic) - if (authorized) { - Log.d(TAG, "Anonymous access granted to topic ${topicUrl(baseUrl, topic)}") - dismiss(authUserId = null) - } else { - Log.w(TAG, "Anonymous access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog") + try { + val authorized = api.checkAnonTopicRead(baseUrl, topic) + if (authorized) { + Log.d(TAG, "Anonymous access granted to topic ${topicUrl(baseUrl, topic)}") + dismiss(authUserId = null) + } else { + Log.w(TAG, "Anonymous access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog") + requireActivity().runOnUiThread { + // Show/hide users dropdown + if (users.isEmpty()) { + loginUsersSpinner.visibility = View.GONE + } else { + val spinnerEntries = users.toMutableList() + spinnerEntries.add(0, User(0, getString(R.string.add_dialog_login_new_user), "")) + loginUsersSpinner.adapter = ArrayAdapter(requireActivity(), R.layout.fragment_add_dialog_dropdown_item, spinnerEntries) + loginUsersSpinner.setSelection(1) + /*loginUsernameText.visibility = View.GONE + loginPasswordText.visibility = View.GONE*/ + } + + // Show login page + subscribeView.visibility = View.GONE + loginProgress.visibility = View.INVISIBLE + loginView.visibility = View.VISIBLE + } + } + } catch (e: Exception) { + Log.w(TAG, "Connection to topic failed: ${e.message}", e) requireActivity().runOnUiThread { - subscribeView.visibility = View.GONE - loginError.visibility = View.INVISIBLE - loginProgress.visibility = View.INVISIBLE - loginView.visibility = View.VISIBLE + subscribeProgress.visibility = View.GONE + subscribeErrorImage.visibility = View.VISIBLE + Toast + .makeText(context, getString(R.string.add_dialog_error_connection_failed, e.message), Toast.LENGTH_LONG) + .show() } } } @@ -281,32 +302,45 @@ class AddFragment : DialogFragment() { private fun checkAuthAndMaybeDismiss(baseUrl: String, topic: String) { loginProgress.visibility = View.VISIBLE - loginError.visibility = View.INVISIBLE - val existingUser = usersSpinner.selectedItem != null && usersSpinner.selectedItem is User && usersSpinner.selectedItemPosition > 0 + loginErrorImage.visibility = View.GONE + val existingUser = loginUsersSpinner.selectedItem != null && loginUsersSpinner.selectedItem is User && loginUsersSpinner.selectedItemPosition > 0 val user = if (existingUser) { - usersSpinner.selectedItem as User + loginUsersSpinner.selectedItem as User } else { User( id = Random.nextLong(), - username = usernameText.text.toString(), - password = passwordText.text.toString() + username = loginUsernameText.text.toString(), + password = loginPasswordText.text.toString() ) } lifecycleScope.launch(Dispatchers.IO) { Log.d(TAG, "Checking read access for user ${user.username} to topic ${topicUrl(baseUrl, topic)}") - val authorized = api.checkUserTopicRead(baseUrl, topic, user.username, user.password) - if (authorized) { - Log.d(TAG, "Access granted for user ${user.username} to topic ${topicUrl(baseUrl, topic)}") - if (!existingUser) { - Log.d(TAG, "Adding new user ${user.username} to database") - repository.addUser(user) + try { + val authorized = api.checkUserTopicRead(baseUrl, topic, user.username, user.password) + if (authorized) { + Log.d(TAG, "Access granted for user ${user.username} to topic ${topicUrl(baseUrl, topic)}") + if (!existingUser) { + Log.d(TAG, "Adding new user ${user.username} to database") + repository.addUser(user) + } + dismiss(authUserId = user.id) + } else { + Log.w(TAG, "Access not allowed for user ${user.username} to topic ${topicUrl(baseUrl, topic)}") + requireActivity().runOnUiThread { + loginProgress.visibility = View.GONE + loginErrorImage.visibility = View.VISIBLE + Toast + .makeText(context, getString(R.string.add_dialog_login_error_not_authorized), Toast.LENGTH_LONG) + .show() + } } - dismiss(authUserId = user.id) - } else { - Log.w(TAG, "Access not allowed for user ${user.username} to topic ${topicUrl(baseUrl, topic)}") + } catch (e: Exception) { requireActivity().runOnUiThread { loginProgress.visibility = View.GONE - loginError.visibility = View.VISIBLE + loginErrorImage.visibility = View.VISIBLE + Toast + .makeText(context, getString(R.string.add_dialog_error_connection_failed, e.message), Toast.LENGTH_LONG) + .show() } } } @@ -314,14 +348,14 @@ class AddFragment : DialogFragment() { private fun validateInput() = lifecycleScope.launch(Dispatchers.IO) { val baseUrl = getBaseUrl() - val topic = topicNameText.text.toString() + val topic = subscribeTopicText.text.toString() val subscription = repository.getSubscription(baseUrl, topic) activity?.let { it.runOnUiThread { if (subscription != null || DISALLOWED_TOPICS.contains(topic)) { subscribeButton.isEnabled = false - } else if (useAnotherServerCheckbox.isChecked) { + } else if (subscribeUseAnotherServerCheckbox.isChecked) { subscribeButton.isEnabled = topic.isNotBlank() && "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic) && baseUrl.isNotBlank() @@ -337,12 +371,12 @@ class AddFragment : DialogFragment() { private fun dismiss(authUserId: Long?) { Log.d(TAG, "Closing dialog and calling onSubscribe handler") requireActivity().runOnUiThread { - val topic = topicNameText.text.toString() + val topic = subscribeTopicText.text.toString() val baseUrl = getBaseUrl() - val instant = if (!BuildConfig.FIREBASE_AVAILABLE || useAnotherServerCheckbox.isChecked) { + val instant = if (!BuildConfig.FIREBASE_AVAILABLE || subscribeUseAnotherServerCheckbox.isChecked) { true } else { - instantDeliveryCheckbox.isChecked + subscribeInstantDeliveryCheckbox.isChecked } subscribeListener.onSubscribe(topic, baseUrl, instant, authUserId = authUserId) dialog?.dismiss() @@ -350,8 +384,8 @@ class AddFragment : DialogFragment() { } private fun getBaseUrl(): String { - return if (useAnotherServerCheckbox.isChecked) { - baseUrlText.text.toString() + return if (subscribeUseAnotherServerCheckbox.isChecked) { + subscribeBaseUrlText.text.toString() } else { getString(R.string.app_base_url) } diff --git a/app/src/main/res/drawable/ic_error_black_24dp.xml b/app/src/main/res/drawable/ic_error_black_24dp.xml new file mode 100644 index 0000000..7fd767b --- /dev/null +++ b/app/src/main/res/drawable/ic_error_black_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" + android:fillColor="#F44336"/> +</vector> diff --git a/app/src/main/res/layout/fragment_add_dialog.xml b/app/src/main/res/layout/fragment_add_dialog.xml index 46a7354..e9cec79 100644 --- a/app/src/main/res/layout/fragment_add_dialog.xml +++ b/app/src/main/res/layout/fragment_add_dialog.xml @@ -1,48 +1,71 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:paddingLeft="16dp" - android:paddingRight="16dp"> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingLeft="16dp" + android:paddingRight="16dp"> - <LinearLayout - android:orientation="vertical" + <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" - android:layout_height="match_parent" android:id="@+id/add_dialog_subscribe_view" tools:visibility="gone"> + android:layout_height="match_parent" + android:orientation="horizontal" + android:id="@+id/add_dialog_subscribe_view" + android:visibility="visible"> <TextView android:id="@+id/add_dialog_title_text" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:paddingTop="16dp" android:paddingBottom="3dp" android:text="@string/add_dialog_title" android:textAlignment="viewStart" - android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"/> + android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toStartOf="@id/add_dialog_error_image"/> <TextView android:text="@string/add_dialog_description_below" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/add_dialog_description_below" - android:paddingStart="4dp" android:paddingTop="3dp"/> + android:paddingStart="4dp" android:paddingTop="3dp" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/add_dialog_title_text"/> <com.google.android.material.textfield.TextInputEditText android:id="@+id/add_dialog_topic_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint" android:importantForAutofill="no" - android:maxLines="1" android:inputType="text" android:maxLength="64"/> + android:maxLines="1" android:inputType="text" android:maxLength="64" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/add_dialog_description_below"/> <CheckBox android:text="@string/add_dialog_use_another_server" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/add_dialog_use_another_server_checkbox" - android:layout_marginTop="-5dp" android:layout_marginBottom="-5dp" android:layout_marginStart="-3dp"/> + android:layout_marginStart="-3dp" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/add_dialog_topic_text" + android:layout_marginTop="-3dp"/> + <ProgressBar + style="?android:attr/progressBarStyle" + android:layout_width="24dp" + android:layout_height="24dp" + android:id="@+id/add_dialog_progress" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/add_dialog_error_image" + app:layout_constraintBottom_toTopOf="@+id/add_dialog_description_below" + android:indeterminate="true" android:layout_marginBottom="5dp" android:visibility="gone"/> <TextView android:text="@string/add_dialog_use_another_server_description" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/add_dialog_use_another_server_description" - android:paddingStart="4dp" android:paddingTop="0dp" android:layout_marginTop="-5dp" - android:visibility="gone"/> + android:paddingStart="4dp" android:paddingTop="0dp" + android:visibility="gone" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/add_dialog_use_another_server_checkbox"/> <com.google.android.material.textfield.TextInputLayout style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu" android:id="@+id/add_dialog_base_url_layout" @@ -54,7 +77,9 @@ android:visibility="gone" app:endIconMode="custom" app:hintEnabled="false" - app:boxBackgroundColor="@android:color/transparent"> + app:boxBackgroundColor="@android:color/transparent" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/add_dialog_use_another_server_description"> <AutoCompleteTextView android:layout_width="match_parent" android:layout_height="wrap_content" @@ -76,7 +101,9 @@ <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" - android:layout_height="wrap_content" android:id="@+id/add_dialog_instant_delivery_box"> + android:layout_height="wrap_content" android:id="@+id/add_dialog_instant_delivery_box" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/add_dialog_base_url_layout" android:layout_marginTop="-3dp"> <CheckBox android:text="@string/add_dialog_instant_delivery" android:layout_width="wrap_content" @@ -95,14 +122,24 @@ android:text="@string/add_dialog_instant_delivery_description" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/add_dialog_instant_delivery_description" - android:paddingStart="4dp" android:paddingTop="0dp" android:layout_marginTop="-5dp" - android:visibility="gone"/> - </LinearLayout> + android:paddingStart="4dp" android:paddingTop="0dp" + android:visibility="gone" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/add_dialog_instant_delivery_box"/> + <ImageView + android:layout_width="24dp" + android:layout_height="24dp" app:srcCompat="@drawable/ic_error_black_24dp" + android:id="@+id/add_dialog_error_image" + app:layout_constraintBottom_toTopOf="@+id/add_dialog_description_below" + android:layout_marginBottom="5dp" app:layout_constraintEnd_toStartOf="@+id/add_dialog_progress" + app:layout_constraintStart_toEndOf="@+id/add_dialog_title_text" android:visibility="gone"/> + </androidx.constraintlayout.widget.ConstraintLayout> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="horizontal" - android:id="@+id/add_dialog_login_view" + android:layout_height="match_parent" + android:orientation="horizontal" + android:id="@+id/add_dialog_login_view" + android:visibility="gone" > <TextView android:id="@+id/add_dialog_login_title" @@ -115,7 +152,7 @@ android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintEnd_toStartOf="@id/add_dialog_login_progress"/> + app:layout_constraintEnd_toStartOf="@id/add_dialog_login_error_image"/> <TextView android:text="This topic requires you to login. Please pick an existing user or type in a username and password." android:layout_width="match_parent" @@ -144,24 +181,22 @@ android:maxLines="1" android:inputType="textPassword" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/add_dialog_login_username"/> - <TextView - android:text="Login failed" - android:layout_width="0dp" - android:layout_height="wrap_content" android:id="@+id/add_dialog_login_error" - android:paddingStart="4dp" android:paddingTop="3dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@+id/add_dialog_login_password" - app:layout_constraintStart_toStartOf="parent" android:visibility="invisible"/> <ProgressBar style="?android:attr/progressBarStyle" android:layout_width="25dp" android:layout_height="25dp" android:id="@+id/add_dialog_login_progress" - app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/add_dialog_login_title" - app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description" android:layout_marginTop="5dp" - android:indeterminate="true"/> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/add_dialog_login_error_image" + app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description" + android:indeterminate="true" android:layout_marginBottom="5dp"/> + <ImageView + android:layout_width="24dp" + android:layout_height="24dp" app:srcCompat="@drawable/ic_error_black_24dp" + android:id="@+id/add_dialog_login_error_image" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description" + app:layout_constraintEnd_toStartOf="@id/add_dialog_login_progress" android:layout_marginBottom="5dp" + app:layout_constraintStart_toEndOf="@+id/add_dialog_login_title"/> </androidx.constraintlayout.widget.ConstraintLayout> - - </LinearLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fbb3b6..185be30 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,14 +78,13 @@ <!-- Add dialog --> <string name="add_dialog_title">Subscribe to topic</string> <string name="add_dialog_description_below"> - Topics are not password-protected, so choose a name that\'s not easy to - guess. Once subscribed, you can PUT/POST to receive notifications on your phone. + Topics may not be password-protected, so choose a name that\'s not easy to + guess. Once subscribed, you can PUT/POST to receive notifications. </string> <string name="add_dialog_topic_name_hint">Topic name, e.g. phils_alerts</string> <string name="add_dialog_use_another_server">Use another server</string> <string name="add_dialog_use_another_server_description"> - You can subscribe to topics from your own server. This option requires a foreground service and - consumes more power, but also delivers notifications faster. + You can subscribe to topics from your own server. This option requires a foreground service. </string> <string name="add_dialog_use_another_server_description_noinstant"> You can subscribe to topics from your own server. Simply type in the base @@ -98,6 +97,9 @@ </string> <string name="add_dialog_button_cancel">Cancel</string> <string name="add_dialog_button_subscribe">Subscribe</string> + <string name="add_dialog_error_connection_failed">Connection failed: %1$s</string> + <string name="add_dialog_login_error_not_authorized">Login failed. User not authorized.</string> + <string name="add_dialog_login_new_user">New user</string> <!-- Detail activity --> <string name="detail_deep_link_subscribed_toast_message">Subscribed to topic %1$s</string> diff --git a/assets/error_black_24dp.svg b/assets/error_black_24dp.svg new file mode 100644 index 0000000..8c61b7a --- /dev/null +++ b/assets/error_black_24dp.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg> \ No newline at end of file