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.view.View import android.widget.* 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.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: TextView 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 // 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 (Fragments need a default constructor) repository = Repository.getInstance(requireActivity()) // 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) // Set "Use another server" description based on flavor useAnotherServerDescription.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) } // 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 || DISALLOWED_TOPICS.contains(topic)) { 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" private val DISALLOWED_TOPICS = listOf("docs", "static") } }