More add dialog changes; we're getting there

This commit is contained in:
Philipp Heckel 2022-01-28 11:42:44 -05:00
parent 88b2576af0
commit c16282fcae
5 changed files with 186 additions and 70 deletions

View file

@ -87,13 +87,7 @@ data class User(
@ColumnInfo(name = "username") val username: String, @ColumnInfo(name = "username") val username: String,
@ColumnInfo(name = "password") val password: String @ColumnInfo(name = "password") val password: String
) { ) {
override fun toString(): String { override fun toString(): String = username
return if (baseUrl == "") {
username
} else {
"$username (${shortUrl(baseUrl)})"
}
}
} }
@Entity(tableName = "Log") @Entity(tableName = "Log")

View file

@ -1,5 +1,6 @@
package io.heckel.ntfy.ui package io.heckel.ntfy.ui
import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
@ -25,6 +26,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.random.Random import kotlin.random.Random
class AddFragment : DialogFragment() { class AddFragment : DialogFragment() {
private val api = ApiService() private val api = ApiService()
@ -33,6 +35,8 @@ class AddFragment : DialogFragment() {
private lateinit var subscribeView: View private lateinit var subscribeView: View
private lateinit var loginView: View private lateinit var loginView: View
private lateinit var positiveButton: Button
private lateinit var negativeButton: Button
// Subscribe page // Subscribe page
private lateinit var subscribeTopicText: TextInputEditText private lateinit var subscribeTopicText: TextInputEditText
@ -45,7 +49,6 @@ class AddFragment : DialogFragment() {
private lateinit var subscribeInstantDeliveryDescription: View private lateinit var subscribeInstantDeliveryDescription: View
private lateinit var subscribeProgress: ProgressBar private lateinit var subscribeProgress: ProgressBar
private lateinit var subscribeErrorImage: View private lateinit var subscribeErrorImage: View
private lateinit var subscribeButton: Button
// Login page // Login page
private lateinit var users: List<User> private lateinit var users: List<User>
@ -79,6 +82,7 @@ class AddFragment : DialogFragment() {
// Main "pages" // Main "pages"
subscribeView = view.findViewById(R.id.add_dialog_subscribe_view) subscribeView = view.findViewById(R.id.add_dialog_subscribe_view)
subscribeView.visibility = View.VISIBLE
loginView = view.findViewById(R.id.add_dialog_login_view) loginView = view.findViewById(R.id.add_dialog_login_view)
loginView.visibility = View.GONE loginView.visibility = View.GONE
@ -153,8 +157,9 @@ class AddFragment : DialogFragment() {
.map { it.key } .map { it.key }
.filterNot { it == appBaseUrl } .filterNot { it == appBaseUrl }
.sorted() .sorted()
val adapter = ArrayAdapter(requireActivity(), R.layout.fragment_add_dialog_dropdown_item, baseUrls) val activity = activity ?: return@launch // We may have pressed "Cancel"
requireActivity().runOnUiThread { val adapter = ArrayAdapter(activity, R.layout.fragment_add_dialog_dropdown_item, baseUrls)
activity.runOnUiThread {
subscribeBaseUrlText.threshold = 1 subscribeBaseUrlText.threshold = 1
subscribeBaseUrlText.setAdapter(adapter) subscribeBaseUrlText.setAdapter(adapter)
if (baseUrls.count() == 1) { if (baseUrls.count() == 1) {
@ -179,17 +184,41 @@ class AddFragment : DialogFragment() {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
if (position == 0) { if (position == 0) {
loginUsernameText.visibility = View.VISIBLE loginUsernameText.visibility = View.VISIBLE
loginUsernameText.isEnabled = true
loginPasswordText.visibility = View.VISIBLE loginPasswordText.visibility = View.VISIBLE
loginPasswordText.isEnabled = true
if (loginUsernameText.requestFocus()) {
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
}
} else { } else {
loginUsernameText.visibility = View.GONE loginUsernameText.visibility = View.GONE
loginUsernameText.isEnabled = false
loginPasswordText.visibility = View.GONE loginPasswordText.visibility = View.GONE
loginPasswordText.isEnabled = false
} }
validateInputLoginView()
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
// This should not happen, ha! // This should not happen, ha!
} }
} }
// Username/password validation on type
val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
validateInputLoginView()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Nothing
}
}
loginUsernameText.addTextChangedListener(textWatcher)
loginPasswordText.addTextChangedListener(textWatcher)
// Build dialog // Build dialog
val dialog = AlertDialog.Builder(activity) val dialog = AlertDialog.Builder(activity)
.setView(view) .setView(view)
@ -197,7 +226,7 @@ class AddFragment : DialogFragment() {
// This will be overridden below to avoid closing the dialog immediately // This will be overridden below to avoid closing the dialog immediately
} }
.setNegativeButton(R.string.add_dialog_button_cancel) { _, _ -> .setNegativeButton(R.string.add_dialog_button_cancel) { _, _ ->
dialog?.cancel() // This will be overridden below
} }
.create() .create()
@ -206,15 +235,18 @@ class AddFragment : DialogFragment() {
// Add logic to disable "Subscribe" button on invalid input // Add logic to disable "Subscribe" button on invalid input
dialog.setOnShowListener { dialog.setOnShowListener {
subscribeButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
subscribeButton.isEnabled = false positiveButton.isEnabled = false
subscribeButton.setOnClickListener { positiveButton.setOnClickListener {
subscribeButtonClick() positiveButtonClick()
}
negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
negativeButtonClick()
} }
val textWatcher = object : TextWatcher { val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
validateInput() validateInputSubscribeView()
} }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing // Nothing
@ -242,7 +274,7 @@ class AddFragment : DialogFragment() {
if (subscribeInstantDeliveryCheckbox.isChecked) subscribeInstantDeliveryDescription.visibility = View.VISIBLE if (subscribeInstantDeliveryCheckbox.isChecked) subscribeInstantDeliveryDescription.visibility = View.VISIBLE
else subscribeInstantDeliveryDescription.visibility = View.GONE else subscribeInstantDeliveryDescription.visibility = View.GONE
} }
validateInput() validateInputSubscribeView()
} }
subscribeUseAnotherServerCheckbox.isChecked = this::baseUrls.isInitialized && baseUrls.count() == 1 subscribeUseAnotherServerCheckbox.isChecked = this::baseUrls.isInitialized && baseUrls.count() == 1
@ -253,19 +285,20 @@ class AddFragment : DialogFragment() {
return dialog return dialog
} }
private fun subscribeButtonClick() { private fun positiveButtonClick() {
val topic = subscribeTopicText.text.toString() val topic = subscribeTopicText.text.toString()
val baseUrl = getBaseUrl() val baseUrl = getBaseUrl()
if (subscribeView.visibility == View.VISIBLE) { if (subscribeView.visibility == View.VISIBLE) {
checkAnonReadAndMaybeShowLogin(baseUrl, topic) checkAnonReadAndMaybeShowLogin(baseUrl, topic)
} else if (loginView.visibility == View.VISIBLE) { } else if (loginView.visibility == View.VISIBLE) {
checkAuthAndMaybeDismiss(baseUrl, topic) loginAndMaybeDismiss(baseUrl, topic)
} }
} }
private fun checkAnonReadAndMaybeShowLogin(baseUrl: String, topic: String) { private fun checkAnonReadAndMaybeShowLogin(baseUrl: String, topic: String) {
subscribeProgress.visibility = View.VISIBLE subscribeProgress.visibility = View.VISIBLE
subscribeErrorImage.visibility = View.GONE subscribeErrorImage.visibility = View.GONE
enableSubscribeView(false)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
Log.d(TAG, "Checking anonymous read access to topic ${topicUrl(baseUrl, topic)}") Log.d(TAG, "Checking anonymous read access to topic ${topicUrl(baseUrl, topic)}")
try { try {
@ -275,29 +308,18 @@ class AddFragment : DialogFragment() {
dismiss(authUserId = null) dismiss(authUserId = null)
} else { } else {
Log.w(TAG, "Anonymous access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog") Log.w(TAG, "Anonymous access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
requireActivity().runOnUiThread { val activity = activity ?: return@launch // We may have pressed "Cancel"
// Show/hide users dropdown activity.runOnUiThread {
val relevantUsers = users.filter { it.baseUrl == baseUrl } showLoginView(activity, baseUrl)
if (relevantUsers.isEmpty()) {
loginUsersSpinner.visibility = View.GONE
} else {
val spinnerEntries = relevantUsers.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)
}
// Show login page
subscribeView.visibility = View.GONE
loginProgress.visibility = View.INVISIBLE
loginView.visibility = View.VISIBLE
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Connection to topic failed: ${e.message}", e) Log.w(TAG, "Connection to topic failed: ${e.message}", e)
requireActivity().runOnUiThread { val activity = activity ?: return@launch // We may have pressed "Cancel"
activity.runOnUiThread {
subscribeProgress.visibility = View.GONE subscribeProgress.visibility = View.GONE
subscribeErrorImage.visibility = View.VISIBLE subscribeErrorImage.visibility = View.VISIBLE
enableSubscribeView(true)
Toast Toast
.makeText(context, getString(R.string.add_dialog_error_connection_failed, e.message), Toast.LENGTH_LONG) .makeText(context, getString(R.string.add_dialog_error_connection_failed, e.message), Toast.LENGTH_LONG)
.show() .show()
@ -306,9 +328,10 @@ class AddFragment : DialogFragment() {
} }
} }
private fun checkAuthAndMaybeDismiss(baseUrl: String, topic: String) { private fun loginAndMaybeDismiss(baseUrl: String, topic: String) {
loginProgress.visibility = View.VISIBLE loginProgress.visibility = View.VISIBLE
loginErrorImage.visibility = View.GONE loginErrorImage.visibility = View.GONE
enableLoginView(false)
val existingUser = loginUsersSpinner.selectedItem != null && loginUsersSpinner.selectedItem is User && loginUsersSpinner.selectedItemPosition > 0 val existingUser = loginUsersSpinner.selectedItem != null && loginUsersSpinner.selectedItem is User && loginUsersSpinner.selectedItemPosition > 0
val user = if (existingUser) { val user = if (existingUser) {
loginUsersSpinner.selectedItem as User loginUsersSpinner.selectedItem as User
@ -333,18 +356,22 @@ class AddFragment : DialogFragment() {
dismiss(authUserId = user.id) dismiss(authUserId = user.id)
} else { } else {
Log.w(TAG, "Access not allowed for user ${user.username} to topic ${topicUrl(baseUrl, topic)}") Log.w(TAG, "Access not allowed for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
requireActivity().runOnUiThread { val activity = activity ?: return@launch // We may have pressed "Cancel"
activity.runOnUiThread {
loginProgress.visibility = View.GONE loginProgress.visibility = View.GONE
loginErrorImage.visibility = View.VISIBLE loginErrorImage.visibility = View.VISIBLE
enableLoginView(true)
Toast Toast
.makeText(context, getString(R.string.add_dialog_login_error_not_authorized), Toast.LENGTH_LONG) .makeText(context, getString(R.string.add_dialog_login_error_not_authorized), Toast.LENGTH_LONG)
.show() .show()
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
requireActivity().runOnUiThread { val activity = activity ?: return@launch // We may have pressed "Cancel"
activity.runOnUiThread {
loginProgress.visibility = View.GONE loginProgress.visibility = View.GONE
loginErrorImage.visibility = View.VISIBLE loginErrorImage.visibility = View.VISIBLE
enableLoginView(true)
Toast Toast
.makeText(context, getString(R.string.add_dialog_error_connection_failed, e.message), Toast.LENGTH_LONG) .makeText(context, getString(R.string.add_dialog_error_connection_failed, e.message), Toast.LENGTH_LONG)
.show() .show()
@ -353,31 +380,51 @@ class AddFragment : DialogFragment() {
} }
} }
private fun validateInput() = lifecycleScope.launch(Dispatchers.IO) { private fun negativeButtonClick() {
val baseUrl = getBaseUrl() if (subscribeView.visibility == View.VISIBLE) {
val topic = subscribeTopicText.text.toString() dialog?.cancel()
val subscription = repository.getSubscription(baseUrl, topic) } else if (loginView.visibility == View.VISIBLE) {
showSubscribeView()
}
}
activity?.let { private fun validateInputSubscribeView() {
it.runOnUiThread { lifecycleScope.launch(Dispatchers.IO) {
if (subscription != null || DISALLOWED_TOPICS.contains(topic)) { val baseUrl = getBaseUrl()
subscribeButton.isEnabled = false val topic = subscribeTopicText.text.toString()
} else if (subscribeUseAnotherServerCheckbox.isChecked) { val subscription = repository.getSubscription(baseUrl, topic)
subscribeButton.isEnabled = topic.isNotBlank()
&& "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic) activity?.let {
&& baseUrl.isNotBlank() it.runOnUiThread {
&& "^https?://.+".toRegex().matches(baseUrl) if (subscription != null || DISALLOWED_TOPICS.contains(topic)) {
} else { positiveButton.isEnabled = false
subscribeButton.isEnabled = topic.isNotBlank() } else if (subscribeUseAnotherServerCheckbox.isChecked) {
&& "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic) positiveButton.isEnabled = topic.isNotBlank()
&& "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic)
&& baseUrl.isNotBlank()
&& "^https?://.+".toRegex().matches(baseUrl)
} else {
positiveButton.isEnabled = topic.isNotBlank()
&& "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic)
}
} }
} }
} }
} }
private fun validateInputLoginView() {
if (loginUsernameText.visibility == View.GONE) {
positiveButton.isEnabled = true
} else {
positiveButton.isEnabled = (loginUsernameText.text?.isNotEmpty() ?: false)
&& (loginPasswordText.text?.isNotEmpty() ?: false)
}
}
private fun dismiss(authUserId: Long?) { private fun dismiss(authUserId: Long?) {
Log.d(TAG, "Closing dialog and calling onSubscribe handler") Log.d(TAG, "Closing dialog and calling onSubscribe handler")
requireActivity().runOnUiThread { val activity = activity?: return // We may have pressed "Cancel"
activity.runOnUiThread {
val topic = subscribeTopicText.text.toString() val topic = subscribeTopicText.text.toString()
val baseUrl = getBaseUrl() val baseUrl = getBaseUrl()
val instant = if (!BuildConfig.FIREBASE_AVAILABLE || subscribeUseAnotherServerCheckbox.isChecked) { val instant = if (!BuildConfig.FIREBASE_AVAILABLE || subscribeUseAnotherServerCheckbox.isChecked) {
@ -398,6 +445,77 @@ 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)
loginView.visibility = View.GONE
subscribeView.visibility = View.VISIBLE
if (subscribeTopicText.requestFocus()) {
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(subscribeTopicText, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun showLoginView(activity: Activity, baseUrl: String) {
resetLoginView()
loginProgress.visibility = View.INVISIBLE
positiveButton.text = getString(R.string.add_dialog_button_login)
negativeButton.text = getString(R.string.add_dialog_button_back)
subscribeView.visibility = View.GONE
loginView.visibility = View.VISIBLE
// Show/hide dropdown
val relevantUsers = users.filter { it.baseUrl == baseUrl }
if (relevantUsers.isEmpty()) {
loginUsersSpinner.visibility = View.GONE
loginUsersSpinner.adapter = ArrayAdapter(activity, R.layout.fragment_add_dialog_dropdown_item, emptyArray<User>())
if (loginUsernameText.requestFocus()) {
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
}
} else {
val spinnerEntries = relevantUsers.toMutableList()
spinnerEntries.add(0, User(0, "", getString(R.string.add_dialog_login_new_user), ""))
loginUsersSpinner.adapter = ArrayAdapter(activity, R.layout.fragment_add_dialog_dropdown_item, spinnerEntries)
loginUsersSpinner.setSelection(1)
}
}
private fun enableSubscribeView(enable: Boolean) {
subscribeTopicText.isEnabled = enable
subscribeBaseUrlText.isEnabled = enable
subscribeInstantDeliveryCheckbox.isEnabled = enable
subscribeUseAnotherServerCheckbox.isEnabled = enable
positiveButton.isEnabled = enable
}
private fun resetSubscribeView() {
subscribeProgress.visibility = View.GONE
subscribeErrorImage.visibility = View.GONE
enableSubscribeView(true)
}
private fun enableLoginView(enable: Boolean) {
loginUsernameText.isEnabled = enable
loginPasswordText.isEnabled = enable
loginUsersSpinner.isEnabled = enable
positiveButton.isEnabled = enable
if (enable && loginUsernameText.requestFocus()) {
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun resetLoginView() {
loginProgress.visibility = View.GONE
loginErrorImage.visibility = View.GONE
loginUsersSpinner.visibility = View.VISIBLE
loginUsernameText.visibility = View.VISIBLE
loginPasswordText.visibility = View.VISIBLE
enableLoginView(true)
}
companion object { companion object {
const val TAG = "NtfyAddFragment" const val TAG = "NtfyAddFragment"
private val DISALLOWED_TOPICS = listOf("docs", "static") private val DISALLOWED_TOPICS = listOf("docs", "static")

View file

@ -12,7 +12,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal"
android:id="@+id/add_dialog_subscribe_view" android:id="@+id/add_dialog_subscribe_view"
android:visibility="visible"> android:visibility="gone">
<TextView <TextView
android:id="@+id/add_dialog_title_text" android:id="@+id/add_dialog_title_text"
android:layout_width="0dp" android:layout_width="0dp"
@ -26,6 +26,15 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/add_dialog_error_image"/> app:layout_constraintEnd_toStartOf="@id/add_dialog_error_image"/>
<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 <TextView
android:text="@string/add_dialog_description_below" android:text="@string/add_dialog_description_below"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -49,15 +58,6 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_dialog_topic_text" app:layout_constraintTop_toBottomOf="@id/add_dialog_topic_text"
android:layout_marginTop="-3dp"/> 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 <TextView
android:text="@string/add_dialog_use_another_server_description" android:text="@string/add_dialog_use_another_server_description"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -65,7 +65,8 @@
android:paddingStart="4dp" android:paddingTop="0dp" android:paddingStart="4dp" android:paddingTop="0dp"
android:visibility="gone" app:layout_constraintStart_toStartOf="parent" android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_dialog_use_another_server_checkbox"/> app:layout_constraintTop_toBottomOf="@id/add_dialog_use_another_server_checkbox"
android:layout_marginTop="-5dp"/>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu"
android:id="@+id/add_dialog_base_url_layout" android:id="@+id/add_dialog_base_url_layout"
@ -139,7 +140,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal"
android:id="@+id/add_dialog_login_view" android:id="@+id/add_dialog_login_view"
android:visibility="gone" android:visibility="visible"
> >
<TextView <TextView
android:id="@+id/add_dialog_login_title" android:id="@+id/add_dialog_login_title"
@ -164,7 +165,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_users_spinner" android:layout_height="wrap_content" android:id="@+id/add_dialog_login_users_spinner"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_description"/> app:layout_constraintTop_toBottomOf="@id/add_dialog_login_description" android:paddingStart="0dp"/>
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/add_dialog_login_username" android:id="@+id/add_dialog_login_username"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -97,6 +97,8 @@
</string> </string>
<string name="add_dialog_button_cancel">Cancel</string> <string name="add_dialog_button_cancel">Cancel</string>
<string name="add_dialog_button_subscribe">Subscribe</string> <string name="add_dialog_button_subscribe">Subscribe</string>
<string name="add_dialog_button_back">Back</string>
<string name="add_dialog_button_login">Login</string>
<string name="add_dialog_error_connection_failed">Connection failed: %1$s</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_error_not_authorized">Login failed. User not authorized.</string>
<string name="add_dialog_login_new_user">New user</string> <string name="add_dialog_login_new_user">New user</string>

View file

@ -1,2 +1,3 @@
Bug fixes: Bug fixes:
* Fix download issues on SDK 29 "Movement not allowed" (#116) * Fix download issues on SDK 29 "Movement not allowed" (#116)
* Fix for Android 12 crashes (#124)