ntfy-android/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt
2021-11-27 16:18:09 -05:00

231 lines
9.7 KiB
Kotlin

package io.heckel.ntfy.ui
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.CheckBox
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Repository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AddFragment : DialogFragment() {
private lateinit var repository: Repository
private lateinit var subscribeListener: SubscribeListener
private lateinit var topicNameText: TextInputEditText
private lateinit var baseUrlLayout: TextInputLayout
private lateinit var baseUrlText: AutoCompleteTextView
private lateinit var useAnotherServerCheckbox: CheckBox
private lateinit var useAnotherServerDescription: View
private lateinit var instantDeliveryBox: View
private lateinit var instantDeliveryCheckbox: CheckBox
private lateinit var instantDeliveryDescription: View
private lateinit var subscribeButton: Button
private lateinit var baseUrls: List<String> // List of base URLs already used, excluding app_base_url
interface SubscribeListener {
fun onSubscribe(topic: String, baseUrl: String, instant: Boolean)
}
override fun onAttach(context: Context) {
super.onAttach(context)
subscribeListener = activity as SubscribeListener
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
if (activity == null) {
throw IllegalStateException("Activity cannot be null")
}
// Dependencies
val database = Database.getInstance(requireActivity().applicationContext)
val sharedPrefs = requireActivity().getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
repository = Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
// Build root view
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_add_dialog, null)
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)
// 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)
} else if (baseUrls.isEmpty()) {
baseUrlLayout.setEndIconDrawable(0)
} else {
baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
}
}
baseUrlLayout.setEndIconOnClickListener {
if (baseUrlText.text.isNotEmpty()) {
baseUrlText.text.clear()
if (baseUrls.isEmpty()) {
baseUrlLayout.setEndIconDrawable(0)
} else {
baseUrlLayout.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()
}
}
baseUrlText.setOnDismissListener { toggleEndIcon() }
baseUrlText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
toggleEndIcon()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Nothing
}
})
// Fill autocomplete for base URL
lifecycleScope.launch(Dispatchers.IO) {
val appBaseUrl = getString(R.string.app_base_url)
baseUrls = repository.getSubscriptions()
.groupBy { it.baseUrl }
.map { it.key }
.filterNot { it == appBaseUrl }
.sorted()
val adapter = ArrayAdapter(requireActivity(), R.layout.fragment_add_dialog_dropdown_item, baseUrls)
requireActivity().runOnUiThread {
baseUrlText.threshold = 1
baseUrlText.setAdapter(adapter)
if (baseUrls.count() == 1) {
baseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
baseUrlText.setText(baseUrls.first())
} else if (baseUrls.count() > 1) {
baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
} else {
baseUrlLayout.setEndIconDrawable(0)
}
}
}
// Show/hide based on flavor
instantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
// Build dialog
val alert = AlertDialog.Builder(activity)
.setView(view)
.setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ ->
val topic = topicNameText.text.toString()
val baseUrl = getBaseUrl()
val instant = if (!BuildConfig.FIREBASE_AVAILABLE || useAnotherServerCheckbox.isChecked) {
true
} else {
instantDeliveryCheckbox.isChecked
}
subscribeListener.onSubscribe(topic, baseUrl, instant)
}
.setNegativeButton(R.string.add_dialog_button_cancel) { _, _ ->
dialog?.cancel()
}
.create()
// Add logic to disable "Subscribe" button on invalid input
alert.setOnShowListener {
val dialog = it as AlertDialog
subscribeButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
subscribeButton.isEnabled = false
val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
validateInput()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Nothing
}
}
topicNameText.addTextChangedListener(textWatcher)
baseUrlText.addTextChangedListener(textWatcher)
instantDeliveryCheckbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) instantDeliveryDescription.visibility = View.VISIBLE
else instantDeliveryDescription.visibility = View.GONE
}
useAnotherServerCheckbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
useAnotherServerDescription.visibility = View.VISIBLE
baseUrlLayout.visibility = View.VISIBLE
instantDeliveryBox.visibility = View.GONE
instantDeliveryDescription.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
}
validateInput()
}
}
return alert
}
private fun validateInput() = lifecycleScope.launch(Dispatchers.IO) {
val baseUrl = getBaseUrl()
val topic = topicNameText.text.toString()
val subscription = repository.getSubscription(baseUrl, topic)
activity?.let {
it.runOnUiThread {
if (subscription != null) {
subscribeButton.isEnabled = false
} else if (useAnotherServerCheckbox.isChecked) {
subscribeButton.isEnabled = topic.isNotBlank()
&& "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic)
&& baseUrl.isNotBlank()
&& "^https?://.+".toRegex().matches(baseUrl)
} else {
subscribeButton.isEnabled = topic.isNotBlank()
&& "[-_A-Za-z0-9]{1,64}".toRegex().matches(topic)
}
}
}
}
private fun getBaseUrl(): String {
return if (useAnotherServerCheckbox.isChecked) {
baseUrlText.text.toString()
} else {
getString(R.string.app_base_url)
}
}
companion object {
const val TAG = "NtfyAddFragment"
}
}