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 // 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" } }