Auto complete home servers

This commit is contained in:
Philipp Heckel 2021-11-25 15:45:12 -05:00
parent 91a29c4d8e
commit 5f15be4bca
7 changed files with 206 additions and 16 deletions

View file

@ -6,25 +6,31 @@ import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Log
import android.view.View import android.view.View
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button import android.widget.Button
import android.widget.CheckBox import android.widget.CheckBox
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputEditText 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.R
import io.heckel.ntfy.data.Database import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.BuildConfig
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AddFragment : DialogFragment() { class AddFragment : DialogFragment() {
private lateinit var repository: Repository private lateinit var repository: Repository
private lateinit var subscribeListener: SubscribeListener private lateinit var subscribeListener: SubscribeListener
private lateinit var topicNameText: TextInputEditText private lateinit var topicNameText: TextInputEditText
private lateinit var baseUrlText: TextInputEditText private lateinit var baseUrlLayout: TextInputLayout
private lateinit var baseUrlText: AutoCompleteTextView
private lateinit var useAnotherServerCheckbox: CheckBox private lateinit var useAnotherServerCheckbox: CheckBox
private lateinit var useAnotherServerDescription: View private lateinit var useAnotherServerDescription: View
private lateinit var instantDeliveryBox: View private lateinit var instantDeliveryBox: View
@ -32,6 +38,8 @@ class AddFragment : DialogFragment() {
private lateinit var instantDeliveryDescription: View private lateinit var instantDeliveryDescription: View
private lateinit var subscribeButton: Button private lateinit var subscribeButton: Button
private lateinit var baseUrls: List<String> // List of base URLs already used, excluding app_base_url
interface SubscribeListener { interface SubscribeListener {
fun onSubscribe(topic: String, baseUrl: String, instant: Boolean) fun onSubscribe(topic: String, baseUrl: String, instant: Boolean)
} }
@ -53,14 +61,73 @@ class AddFragment : DialogFragment() {
// Build root view // Build root view
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_add_dialog, null) val view = requireActivity().layoutInflater.inflate(R.layout.fragment_add_dialog, null)
topicNameText = view.findViewById(R.id.add_dialog_topic_text) as TextInputEditText topicNameText = view.findViewById(R.id.add_dialog_topic_text)
baseUrlText = view.findViewById(R.id.add_dialog_base_url_text) as TextInputEditText 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) instantDeliveryBox = view.findViewById(R.id.add_dialog_instant_delivery_box)
instantDeliveryCheckbox = view.findViewById(R.id.add_dialog_instant_delivery_checkbox) as CheckBox instantDeliveryCheckbox = view.findViewById(R.id.add_dialog_instant_delivery_checkbox)
instantDeliveryDescription = view.findViewById(R.id.add_dialog_instant_delivery_description) instantDeliveryDescription = view.findViewById(R.id.add_dialog_instant_delivery_description)
useAnotherServerCheckbox = view.findViewById(R.id.add_dialog_use_another_server_checkbox) as CheckBox useAnotherServerCheckbox = view.findViewById(R.id.add_dialog_use_another_server_checkbox)
useAnotherServerDescription = view.findViewById(R.id.add_dialog_use_another_server_description) 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) {
// Sort existing base URLs by most frequently used
val appBaseUrl = getString(R.string.app_base_url)
baseUrls = repository.getSubscriptions()
.groupBy { it.baseUrl }
.mapValues { it.value.size }
.toList()
.sortedBy { (_, size) -> size }
.reversed()
.map { (baseUrl, _) -> baseUrl }
.filterNot { it == appBaseUrl }
val adapter = ArrayAdapter(requireActivity(), R.layout.fragment_add_dialog_dropdown_item, baseUrls)
requireActivity().runOnUiThread {
baseUrlText.threshold = 1
baseUrlText.setAdapter(adapter)
if (baseUrls.isNotEmpty()) {
baseUrlText.setText(baseUrls.first())
}
}
}
// Show/hide based on flavor // Show/hide based on flavor
instantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE instantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
@ -109,12 +176,12 @@ class AddFragment : DialogFragment() {
useAnotherServerCheckbox.setOnCheckedChangeListener { _, isChecked -> useAnotherServerCheckbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) { if (isChecked) {
useAnotherServerDescription.visibility = View.VISIBLE useAnotherServerDescription.visibility = View.VISIBLE
baseUrlText.visibility = View.VISIBLE baseUrlLayout.visibility = View.VISIBLE
instantDeliveryBox.visibility = View.GONE instantDeliveryBox.visibility = View.GONE
instantDeliveryDescription.visibility = View.GONE instantDeliveryDescription.visibility = View.GONE
} else { } else {
useAnotherServerDescription.visibility = View.GONE useAnotherServerDescription.visibility = View.GONE
baseUrlText.visibility = View.GONE baseUrlLayout.visibility = View.GONE
instantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE instantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
if (instantDeliveryCheckbox.isChecked) instantDeliveryDescription.visibility = View.VISIBLE if (instantDeliveryCheckbox.isChecked) instantDeliveryDescription.visibility = View.VISIBLE
else instantDeliveryDescription.visibility = View.GONE else instantDeliveryDescription.visibility = View.GONE

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp"
tools:ignore="NewApi">
<path
android:fillColor="#888888"
android:pathData="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/>
</vector>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp"
tools:ignore="NewApi">
<path
android:fillColor="#888888"
android:pathData="M7 10l5 5 5-5z"/>
</vector>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp"
tools:ignore="NewApi">
<path
android:fillColor="#888888"
android:pathData="M7 14l5-5 5 5z"/>
</vector>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" 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:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:paddingLeft="16dp" android:paddingLeft="16dp"
@ -37,12 +38,37 @@
android:layout_height="wrap_content" android:id="@+id/add_dialog_use_another_server_description" 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:paddingStart="4dp" android:paddingTop="0dp" android:layout_marginTop="-5dp"
android:visibility="gone"/> android:visibility="gone"/>
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputLayout
android:id="@+id/add_dialog_base_url_text" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu"
android:id="@+id/add_dialog_base_url_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:visibility="gone" android:layout_height="wrap_content"
android:hint="@string/app_base_url" android:inputType="textUri" android:maxLines="1" android:layout_margin="0dp"
android:layout_marginTop="-2dp" android:layout_marginBottom="5dp"/> android:background="@android:color/transparent"
android:padding="0dp"
android:visibility="gone"
app:endIconMode="custom"
app:hintEnabled="false"
app:boxBackgroundColor="@android:color/transparent">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/add_dialog_base_url_text"
android:hint="@string/app_base_url"
android:maxLines="1"
android:layout_marginTop="-2dp"
android:layout_marginBottom="5dp"
android:visibility="visible"
android:inputType="textNoSuggestions"
android:textAppearance="?android:attr/textAppearanceMedium"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:paddingTop="10dp"
android:paddingBottom="0dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"/>
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout <LinearLayout
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorAlertDialogListItem"
android:gravity="center_vertical"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:ellipsize="marquee"
/>

View file

@ -60,8 +60,8 @@
<string name="add_dialog_topic_name_hint">Topic name, e.g. phils_alerts</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">Use another server</string>
<string name="add_dialog_use_another_server_description"> <string name="add_dialog_use_another_server_description">
You can subscribe to topics from your own server. Due to platform limitations, this option requires a foreground You can subscribe to topics from your own server. This option requires a foreground service and
service and consumes more power, but also delivers notifications faster (even in doze mode). consumes more power, but also delivers notifications faster (even in doze mode).
</string> </string>
<string name="add_dialog_instant_delivery">Instant delivery in doze mode</string> <string name="add_dialog_instant_delivery">Instant delivery in doze mode</string>
<string name="add_dialog_instant_delivery_description"> <string name="add_dialog_instant_delivery_description">