User management works
This commit is contained in:
parent
43757eb7b5
commit
c67af7f958
7 changed files with 139 additions and 31 deletions
|
@ -265,6 +265,9 @@ interface SubscriptionDao {
|
||||||
|
|
||||||
@Query("DELETE FROM subscription WHERE id = :subscriptionId")
|
@Query("DELETE FROM subscription WHERE id = :subscriptionId")
|
||||||
fun remove(subscriptionId: Long)
|
fun remove(subscriptionId: Long)
|
||||||
|
|
||||||
|
@Query("UPDATE subscription SET authUserId = null WHERE authUserId = :authUserId")
|
||||||
|
fun removeAuthUserFromSubscriptions(authUserId: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
@ -311,8 +314,8 @@ interface UserDao {
|
||||||
@Update
|
@Update
|
||||||
suspend fun update(user: User)
|
suspend fun update(user: User)
|
||||||
|
|
||||||
@Delete
|
@Query("DELETE FROM user WHERE id = :id")
|
||||||
suspend fun delete(user: User)
|
suspend fun delete(id: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
|
|
@ -84,6 +84,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
subscriptionDao.remove(subscriptionId)
|
subscriptionDao.remove(subscriptionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun removeAuthUserFromSubscriptions(authUserId: Long) {
|
||||||
|
subscriptionDao.removeAuthUserFromSubscriptions(authUserId)
|
||||||
|
}
|
||||||
|
|
||||||
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
|
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
|
||||||
return notificationDao.listFlow(subscriptionId).asLiveData()
|
return notificationDao.listFlow(subscriptionId).asLiveData()
|
||||||
}
|
}
|
||||||
|
@ -137,13 +141,21 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addUser(user: User) {
|
suspend fun addUser(user: User) {
|
||||||
return userDao.insert(user)
|
userDao.insert(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateUser(user: User) {
|
||||||
|
userDao.update(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUser(userId: Long): User {
|
suspend fun getUser(userId: Long): User {
|
||||||
return userDao.get(userId)
|
return userDao.get(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun deleteUser(userId: Long) {
|
||||||
|
userDao.delete(userId)
|
||||||
|
}
|
||||||
|
|
||||||
fun getPollWorkerVersion(): Int {
|
fun getPollWorkerVersion(): Int {
|
||||||
return sharedPrefs.getInt(SHARED_PREFS_POLL_WORKER_VERSION, 0)
|
return sharedPrefs.getInt(SHARED_PREFS_POLL_WORKER_VERSION, 0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,12 @@ class SubscriberServiceManager(private val context: Context) {
|
||||||
workManager.enqueue(startServiceRequest)
|
workManager.enqueue(startServiceRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
Intent(context, SubscriberService::class.java).also { intent ->
|
||||||
|
context.stopService(intent) // Service will auto-restart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts or stops the foreground service by figuring out how many instant delivery subscriptions
|
* Starts or stops the foreground service by figuring out how many instant delivery subscriptions
|
||||||
* exist. If there's > 0, then we need a foreground service.
|
* exist. If there's > 0, then we need a foreground service.
|
||||||
|
|
|
@ -15,6 +15,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import androidx.preference.Preference.OnPreferenceClickListener
|
import androidx.preference.Preference.OnPreferenceClickListener
|
||||||
|
@ -25,6 +26,7 @@ import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.User
|
import io.heckel.ntfy.db.User
|
||||||
import io.heckel.ntfy.log.Log
|
import io.heckel.ntfy.log.Log
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
|
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||||
import io.heckel.ntfy.util.formatBytes
|
import io.heckel.ntfy.util.formatBytes
|
||||||
import io.heckel.ntfy.util.formatDateShort
|
import io.heckel.ntfy.util.formatDateShort
|
||||||
import io.heckel.ntfy.util.shortUrl
|
import io.heckel.ntfy.util.shortUrl
|
||||||
|
@ -43,8 +45,13 @@ import java.util.concurrent.TimeUnit
|
||||||
* The "nested screen" navigation stuff (for user management) has been taken from
|
* The "nested screen" navigation stuff (for user management) has been taken from
|
||||||
* https://github.com/googlearchive/android-preferences/blob/master/app/src/main/java/com/example/androidx/preference/sample/MainActivity.kt
|
* https://github.com/googlearchive/android-preferences/blob/master/app/src/main/java/com/example/androidx/preference/sample/MainActivity.kt
|
||||||
*/
|
*/
|
||||||
class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
|
||||||
private lateinit var fragment: SettingsFragment
|
UserFragment.UserDialogListener {
|
||||||
|
private lateinit var settingsFragment: SettingsFragment
|
||||||
|
private lateinit var userSettingsFragment: UserSettingsFragment
|
||||||
|
|
||||||
|
private lateinit var repository: Repository
|
||||||
|
private lateinit var serviceManager: SubscriberServiceManager
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -52,11 +59,14 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
|
|
||||||
Log.d(TAG, "Create $this")
|
Log.d(TAG, "Create $this")
|
||||||
|
|
||||||
|
repository = Repository.getInstance(this)
|
||||||
|
serviceManager = SubscriberServiceManager(this)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
fragment = SettingsFragment() // Empty constructor!
|
settingsFragment = SettingsFragment() // Empty constructor!
|
||||||
supportFragmentManager
|
supportFragmentManager
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(R.id.settings_layout, fragment)
|
.replace(R.id.settings_layout, settingsFragment)
|
||||||
.commit()
|
.commit()
|
||||||
} else {
|
} else {
|
||||||
title = savedInstanceState.getCharSequence(TITLE_TAG)
|
title = savedInstanceState.getCharSequence(TITLE_TAG)
|
||||||
|
@ -84,7 +94,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
return super.onSupportNavigateUp()
|
return super.onSupportNavigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onPreferenceStartFragment(
|
override fun onPreferenceStartFragment(
|
||||||
caller: PreferenceFragmentCompat,
|
caller: PreferenceFragmentCompat,
|
||||||
pref: Preference
|
pref: Preference
|
||||||
|
@ -98,17 +107,25 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
arguments = args
|
arguments = args
|
||||||
setTargetFragment(caller, 0)
|
setTargetFragment(caller, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the existing Fragment with the new Fragment
|
// Replace the existing Fragment with the new Fragment
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.settings_layout, fragment)
|
.replace(R.id.settings_layout, fragment)
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commit()
|
.commit()
|
||||||
title = pref.title
|
title = pref.title
|
||||||
|
|
||||||
|
// Save user settings fragment for later
|
||||||
|
if (fragment is UserSettingsFragment) {
|
||||||
|
userSettingsFragment = fragment
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
private lateinit var repository: Repository
|
private lateinit var repository: Repository
|
||||||
|
private lateinit var serviceManager: SubscriberServiceManager
|
||||||
private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET
|
private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
@ -116,6 +133,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
|
|
||||||
// Dependencies (Fragments need a default constructor)
|
// Dependencies (Fragments need a default constructor)
|
||||||
repository = Repository.getInstance(requireActivity())
|
repository = Repository.getInstance(requireActivity())
|
||||||
|
serviceManager = SubscriberServiceManager(requireActivity())
|
||||||
autoDownloadSelection = repository.getAutoDownloadMaxSize() // Only used for <= Android P, due to permissions request
|
autoDownloadSelection = repository.getAutoDownloadMaxSize() // Only used for <= Android P, due to permissions request
|
||||||
|
|
||||||
// Important note: We do not use the default shared prefs to store settings. Every
|
// Important note: We do not use the default shared prefs to store settings. Every
|
||||||
|
@ -421,10 +439,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restartService() {
|
private fun restartService() {
|
||||||
val context = this@SettingsFragment.context
|
serviceManager.stop() // Service will auto-restart
|
||||||
Intent(context, SubscriberService::class.java).also { intent ->
|
|
||||||
context?.stopService(intent) // Service will auto-restart
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyLogsToClipboard() {
|
private fun copyLogsToClipboard() {
|
||||||
|
@ -516,10 +531,17 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.user_preferences, rootKey)
|
setPreferencesFromResource(R.xml.user_preferences, rootKey)
|
||||||
|
|
||||||
// Dependencies (Fragments need a default constructor)
|
|
||||||
repository = Repository.getInstance(requireActivity())
|
repository = Repository.getInstance(requireActivity())
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UserWithMetadata(
|
||||||
|
val user: User,
|
||||||
|
val topics: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
preferenceScreen.removeAll()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val userIdsWithTopics = repository.getSubscriptions()
|
val userIdsWithTopics = repository.getSubscriptions()
|
||||||
.groupBy { it.authUserId }
|
.groupBy { it.authUserId }
|
||||||
|
@ -530,18 +552,12 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
UserWithMetadata(user, topics)
|
UserWithMetadata(user, topics)
|
||||||
}
|
}
|
||||||
.groupBy { it.user.baseUrl }
|
.groupBy { it.user.baseUrl }
|
||||||
|
|
||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
addUserPreferences(usersByBaseUrl)
|
addUserPreferences(usersByBaseUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UserWithMetadata(
|
|
||||||
val user: User,
|
|
||||||
val topics: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun addUserPreferences(usersByBaseUrl: Map<String, List<UserWithMetadata>>) {
|
private fun addUserPreferences(usersByBaseUrl: Map<String, List<UserWithMetadata>>) {
|
||||||
usersByBaseUrl.forEach { entry ->
|
usersByBaseUrl.forEach { entry ->
|
||||||
val baseUrl = entry.key
|
val baseUrl = entry.key
|
||||||
|
@ -574,9 +590,14 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user
|
// Add user
|
||||||
val preference = Preference(preferenceScreen.context)
|
val userAddCategory = PreferenceCategory(preferenceScreen.context)
|
||||||
preference.title = getString(R.string.settings_users_prefs_user_add)
|
userAddCategory.title = getString(R.string.settings_users_prefs_user_add)
|
||||||
preference.onPreferenceClickListener = OnPreferenceClickListener { _ ->
|
preferenceScreen.addPreference(userAddCategory)
|
||||||
|
|
||||||
|
val userAddPref = Preference(preferenceScreen.context)
|
||||||
|
userAddPref.title = getString(R.string.settings_users_prefs_user_add_title)
|
||||||
|
userAddPref.summary = getString(R.string.settings_users_prefs_user_add_summary)
|
||||||
|
userAddPref.onPreferenceClickListener = OnPreferenceClickListener { _ ->
|
||||||
activity?.let {
|
activity?.let {
|
||||||
UserFragment
|
UserFragment
|
||||||
.newInstance(user = null)
|
.newInstance(user = null)
|
||||||
|
@ -584,7 +605,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
preferenceScreen.addPreference(preference)
|
userAddCategory.addPreference(userAddPref)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -597,9 +618,39 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAddUser(dialog: DialogFragment, user: User) {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
repository.addUser(user) // New users are not used, so no service refresh required
|
||||||
|
runOnUiThread {
|
||||||
|
userSettingsFragment.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpdateUser(dialog: DialogFragment, user: User) {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
repository.updateUser(user)
|
||||||
|
serviceManager.stop() // Editing does not change the user ID
|
||||||
|
runOnUiThread {
|
||||||
|
userSettingsFragment.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteUser(dialog: DialogFragment, authUserId: Long) {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
repository.removeAuthUserFromSubscriptions(authUserId)
|
||||||
|
repository.deleteUser(authUserId)
|
||||||
|
serviceManager.refresh() // authUserId changed, so refresh is enough
|
||||||
|
runOnUiThread {
|
||||||
|
userSettingsFragment.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setAutoDownload() {
|
private fun setAutoDownload() {
|
||||||
if (!this::fragment.isInitialized) return
|
if (!this::settingsFragment.isInitialized) return
|
||||||
fragment.setAutoDownload()
|
settingsFragment.setAutoDownload()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package io.heckel.ntfy.ui
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
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
|
||||||
|
@ -14,15 +15,28 @@ import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.User
|
import io.heckel.ntfy.db.User
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
class UserFragment : DialogFragment() {
|
class UserFragment : DialogFragment() {
|
||||||
private var user: User? = null
|
private var user: User? = null
|
||||||
|
private lateinit var listener: UserDialogListener
|
||||||
|
|
||||||
private lateinit var baseUrlView: TextInputEditText
|
private lateinit var baseUrlView: TextInputEditText
|
||||||
private lateinit var usernameView: TextInputEditText
|
private lateinit var usernameView: TextInputEditText
|
||||||
private lateinit var passwordView: TextInputEditText
|
private lateinit var passwordView: TextInputEditText
|
||||||
private lateinit var positiveButton: Button
|
private lateinit var positiveButton: Button
|
||||||
|
|
||||||
|
interface UserDialogListener {
|
||||||
|
fun onAddUser(dialog: DialogFragment, user: User)
|
||||||
|
fun onUpdateUser(dialog: DialogFragment, user: User)
|
||||||
|
fun onDeleteUser(dialog: DialogFragment, authUserId: Long)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
listener = activity as UserDialogListener
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Reconstruct user (if it is present in the bundle)
|
// Reconstruct user (if it is present in the bundle)
|
||||||
val userId = arguments?.getLong(BUNDLE_USER_ID)
|
val userId = arguments?.getLong(BUNDLE_USER_ID)
|
||||||
|
@ -62,14 +76,16 @@ class UserFragment : DialogFragment() {
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setPositiveButton(positiveButtonTextResId) { _, _ ->
|
.setPositiveButton(positiveButtonTextResId) { _, _ ->
|
||||||
// This will be overridden below to avoid closing the dialog immediately
|
saveClicked()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.user_dialog_button_cancel) { _, _ ->
|
.setNegativeButton(R.string.user_dialog_button_cancel) { _, _ ->
|
||||||
// This will be overridden below
|
// Do nothing
|
||||||
}
|
}
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
builder.setNeutralButton(R.string.user_dialog_button_delete) { _, _ ->
|
builder.setNeutralButton(R.string.user_dialog_button_delete) { _, _ ->
|
||||||
// This will be overridden below
|
if (this::listener.isInitialized && userId != null) {
|
||||||
|
listener.onDeleteUser(this, userId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
@ -109,6 +125,24 @@ class UserFragment : DialogFragment() {
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveClicked() {
|
||||||
|
if (!this::listener.isInitialized) return
|
||||||
|
val baseUrl = baseUrlView.text?.toString() ?: ""
|
||||||
|
val username = usernameView.text?.toString() ?: ""
|
||||||
|
val password = passwordView.text?.toString() ?: ""
|
||||||
|
if (user == null) {
|
||||||
|
user = User(Random.nextLong(), baseUrl, username, password)
|
||||||
|
listener.onAddUser(this, user!!)
|
||||||
|
} else {
|
||||||
|
user = if (password.isNotEmpty()) {
|
||||||
|
user!!.copy(username = username, password = password)
|
||||||
|
} else {
|
||||||
|
user!!.copy(username = username)
|
||||||
|
}
|
||||||
|
listener.onUpdateUser(this, user!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun validateInput() {
|
private fun validateInput() {
|
||||||
val baseUrl = baseUrlView.text?.toString() ?: ""
|
val baseUrl = baseUrlView.text?.toString() ?: ""
|
||||||
val username = usernameView.text?.toString() ?: ""
|
val username = usernameView.text?.toString() ?: ""
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
android:paddingRight="16dp"
|
android:paddingRight="16dp"
|
||||||
android:visibility="visible">
|
android:visibility="visible" android:paddingBottom="10dp">
|
||||||
<TextView
|
<TextView
|
||||||
android:text="This topic requires you to login. Please pick an existing user or type in a username and password."
|
android:text="This topic requires you to login. Please pick an existing user or type in a username and password."
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -246,7 +246,9 @@
|
||||||
<string name="settings_users_prefs_user_not_used">Not used by any topics</string>
|
<string name="settings_users_prefs_user_not_used">Not used by any topics</string>
|
||||||
<string name="settings_users_prefs_user_used_by_one">Used by topic %1$s</string>
|
<string name="settings_users_prefs_user_used_by_one">Used by topic %1$s</string>
|
||||||
<string name="settings_users_prefs_user_used_by_many">Used by topics %1$s</string>
|
<string name="settings_users_prefs_user_used_by_many">Used by topics %1$s</string>
|
||||||
<string name="settings_users_prefs_user_add">Add user</string>
|
<string name="settings_users_prefs_user_add">Add users</string>
|
||||||
|
<string name="settings_users_prefs_user_add_title">Add new user</string>
|
||||||
|
<string name="settings_users_prefs_user_add_summary">Create a new user that can be associated to topics. You can also create a new user when adding a topic.</string>
|
||||||
<string name="settings_unified_push_header">UnifiedPush</string>
|
<string name="settings_unified_push_header">UnifiedPush</string>
|
||||||
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
|
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
|
||||||
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>
|
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>
|
||||||
|
|
Loading…
Reference in a new issue