Message bar
This commit is contained in:
parent
d3f83001ce
commit
77e58d518b
15 changed files with 739 additions and 6 deletions
|
|
@ -333,6 +333,16 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
}
|
||||
}
|
||||
|
||||
fun getMessageBarEnabled(): Boolean {
|
||||
return sharedPrefs.getBoolean(SHARED_PREFS_MESSAGE_BAR_ENABLED, false) // Disabled by default (show FAB)
|
||||
}
|
||||
|
||||
fun setMessageBarEnabled(enabled: Boolean) {
|
||||
sharedPrefs.edit {
|
||||
putBoolean(SHARED_PREFS_MESSAGE_BAR_ENABLED, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBatteryOptimizationsRemindTime(): Long {
|
||||
return sharedPrefs.getLong(SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME, BATTERY_OPTIMIZATIONS_REMIND_TIME_ALWAYS)
|
||||
}
|
||||
|
|
@ -511,6 +521,7 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
const val SHARED_PREFS_UNIFIEDPUSH_ENABLED = "UnifiedPushEnabled"
|
||||
const val SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED = "InsistentMaxPriority"
|
||||
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
|
||||
const val SHARED_PREFS_MESSAGE_BAR_ENABLED = "MessageBarEnabled"
|
||||
const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
|
||||
const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner)
|
||||
const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime"
|
||||
|
|
|
|||
|
|
@ -57,8 +57,13 @@ import kotlin.random.Random
|
|||
import androidx.core.view.size
|
||||
import androidx.core.view.get
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import android.widget.ImageButton
|
||||
|
||||
class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSettingsListener {
|
||||
class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSettingsListener, PublishFragment.PublishListener {
|
||||
private val viewModel by viewModels<DetailViewModel> {
|
||||
DetailViewModelFactory((application as Application).repository)
|
||||
}
|
||||
|
|
@ -81,6 +86,11 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
|
|||
private lateinit var mainList: RecyclerView
|
||||
private lateinit var mainListContainer: SwipeRefreshLayout
|
||||
private lateinit var menu: Menu
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var messageBar: View
|
||||
private lateinit var messageBarText: TextInputEditText
|
||||
private lateinit var messageBarSendButton: ImageButton
|
||||
private lateinit var messageBarExpandButton: ImageButton
|
||||
|
||||
// Action mode stuff
|
||||
private var actionMode: ActionMode? = null
|
||||
|
|
@ -345,6 +355,109 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
|
|||
} catch (_: Exception) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
// Setup FAB and message bar
|
||||
setupPublishUI()
|
||||
}
|
||||
|
||||
private fun setupPublishUI() {
|
||||
fab = findViewById(R.id.detail_fab)
|
||||
messageBar = findViewById(R.id.detail_message_bar)
|
||||
messageBarText = messageBar.findViewById(R.id.message_bar_text)
|
||||
messageBarSendButton = messageBar.findViewById(R.id.message_bar_send_button)
|
||||
messageBarExpandButton = messageBar.findViewById(R.id.message_bar_expand_button)
|
||||
|
||||
val messageBarEnabled = repository.getMessageBarEnabled()
|
||||
|
||||
if (messageBarEnabled) {
|
||||
// Show message bar, hide FAB
|
||||
fab.visibility = View.GONE
|
||||
messageBar.visibility = View.VISIBLE
|
||||
|
||||
// Send button click
|
||||
messageBarSendButton.setOnClickListener {
|
||||
val message = messageBarText.text.toString()
|
||||
if (message.isNotEmpty()) {
|
||||
publishMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Expand button click - open full dialog
|
||||
messageBarExpandButton.setOnClickListener {
|
||||
openPublishDialog(messageBarText.text.toString())
|
||||
}
|
||||
} else {
|
||||
// Show FAB, hide message bar
|
||||
fab.visibility = View.VISIBLE
|
||||
messageBar.visibility = View.GONE
|
||||
|
||||
fab.setOnClickListener {
|
||||
openPublishDialog("")
|
||||
}
|
||||
|
||||
// Add bottom padding to FAB to account for navigation bar
|
||||
ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val layoutParams = view.layoutParams as androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams
|
||||
layoutParams.bottomMargin = systemBars.bottom + resources.getDimensionPixelSize(R.dimen.fab_margin)
|
||||
view.layoutParams = layoutParams
|
||||
insets
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPublishDialog(initialMessage: String) {
|
||||
val fragment = PublishFragment.newInstance(subscriptionBaseUrl, subscriptionTopic, initialMessage)
|
||||
fragment.show(supportFragmentManager, PublishFragment.TAG)
|
||||
}
|
||||
|
||||
private fun publishMessage(message: String) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val user = repository.getUser(subscriptionBaseUrl)
|
||||
api.publish(
|
||||
baseUrl = subscriptionBaseUrl,
|
||||
topic = subscriptionTopic,
|
||||
user = user,
|
||||
message = message,
|
||||
title = "",
|
||||
priority = 3, // Default priority
|
||||
tags = emptyList(),
|
||||
delay = ""
|
||||
)
|
||||
runOnUiThread {
|
||||
messageBarText.text?.clear()
|
||||
Toast.makeText(this@DetailActivity, R.string.publish_dialog_message_published, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to publish message", e)
|
||||
runOnUiThread {
|
||||
val errorMessage = when (e) {
|
||||
is ApiService.UnauthorizedException -> {
|
||||
if (e.user != null) {
|
||||
getString(R.string.detail_test_message_error_unauthorized_user, e.user.username)
|
||||
} else {
|
||||
getString(R.string.detail_test_message_error_unauthorized_anon)
|
||||
}
|
||||
}
|
||||
is ApiService.EntityTooLargeException -> {
|
||||
getString(R.string.detail_test_message_error_too_large)
|
||||
}
|
||||
else -> {
|
||||
getString(R.string.publish_dialog_error_sending, e.message)
|
||||
}
|
||||
}
|
||||
Toast.makeText(this@DetailActivity, errorMessage, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPublished() {
|
||||
// Clear the message bar text when a message is published from the dialog
|
||||
if (this::messageBarText.isInitialized) {
|
||||
messageBarText.text?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
|
|
|||
252
app/src/main/java/io/heckel/ntfy/ui/PublishFragment.kt
Normal file
252
app/src/main/java/io/heckel/ntfy/ui/PublishFragment.kt
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import io.heckel.ntfy.util.Log
|
||||
import io.heckel.ntfy.util.AfterChangedTextWatcher
|
||||
import io.heckel.ntfy.util.topicShortUrl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PublishFragment : DialogFragment() {
|
||||
private val api = ApiService()
|
||||
|
||||
private lateinit var repository: Repository
|
||||
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var sendMenuItem: MenuItem
|
||||
private lateinit var titleText: TextInputEditText
|
||||
private lateinit var messageText: TextInputEditText
|
||||
private lateinit var tagsText: TextInputEditText
|
||||
private lateinit var priorityDropdown: AutoCompleteTextView
|
||||
private lateinit var progress: ProgressBar
|
||||
private lateinit var errorText: TextView
|
||||
private lateinit var errorImage: View
|
||||
|
||||
private var baseUrl: String = ""
|
||||
private var topic: String = ""
|
||||
private var selectedPriority: Int = 3 // Default priority
|
||||
|
||||
private var initialMessage: String = ""
|
||||
|
||||
interface PublishListener {
|
||||
fun onPublished()
|
||||
}
|
||||
|
||||
private var publishListener: PublishListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
if (context is PublishListener) {
|
||||
publishListener = context
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
if (activity == null) {
|
||||
throw IllegalStateException("Activity cannot be null")
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
repository = Repository.getInstance(requireActivity())
|
||||
|
||||
// Get arguments
|
||||
baseUrl = arguments?.getString(ARG_BASE_URL) ?: ""
|
||||
topic = arguments?.getString(ARG_TOPIC) ?: ""
|
||||
initialMessage = arguments?.getString(ARG_MESSAGE) ?: ""
|
||||
|
||||
// Build root view
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_publish_dialog, null)
|
||||
|
||||
// Setup toolbar
|
||||
toolbar = view.findViewById(R.id.publish_dialog_toolbar)
|
||||
toolbar.title = getString(R.string.publish_dialog_title, topicShortUrl(baseUrl, topic))
|
||||
toolbar.setNavigationOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
toolbar.setOnMenuItemClickListener { menuItem ->
|
||||
if (menuItem.itemId == R.id.publish_dialog_send_button) {
|
||||
onSendClick()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
sendMenuItem = toolbar.menu.findItem(R.id.publish_dialog_send_button)
|
||||
|
||||
// Fields
|
||||
titleText = view.findViewById(R.id.publish_dialog_title_text)
|
||||
messageText = view.findViewById(R.id.publish_dialog_message_text)
|
||||
tagsText = view.findViewById(R.id.publish_dialog_tags_text)
|
||||
priorityDropdown = view.findViewById(R.id.publish_dialog_priority_dropdown)
|
||||
progress = view.findViewById(R.id.publish_dialog_progress)
|
||||
errorText = view.findViewById(R.id.publish_dialog_error_text)
|
||||
errorImage = view.findViewById(R.id.publish_dialog_error_image)
|
||||
|
||||
// Set initial message if provided
|
||||
if (initialMessage.isNotEmpty()) {
|
||||
messageText.setText(initialMessage)
|
||||
}
|
||||
|
||||
// Setup priority dropdown
|
||||
val priorities = listOf(
|
||||
getString(R.string.publish_dialog_priority_min),
|
||||
getString(R.string.publish_dialog_priority_low),
|
||||
getString(R.string.publish_dialog_priority_default),
|
||||
getString(R.string.publish_dialog_priority_high),
|
||||
getString(R.string.publish_dialog_priority_max)
|
||||
)
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, priorities)
|
||||
priorityDropdown.setAdapter(adapter)
|
||||
priorityDropdown.setText(priorities[2], false) // Default priority
|
||||
priorityDropdown.setOnItemClickListener { _, _, position, _ ->
|
||||
selectedPriority = position + 1 // Priority is 1-5
|
||||
}
|
||||
|
||||
// Validation on text change
|
||||
val textWatcher = AfterChangedTextWatcher {
|
||||
validateInput()
|
||||
}
|
||||
messageText.addTextChangedListener(textWatcher)
|
||||
|
||||
// Build dialog
|
||||
val dialog = Dialog(requireContext(), R.style.Theme_App_FullScreenDialog)
|
||||
dialog.setContentView(view)
|
||||
|
||||
// Initial validation
|
||||
validateInput()
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialog?.window?.apply {
|
||||
setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Show keyboard after the dialog is fully visible
|
||||
messageText.postDelayed({
|
||||
messageText.requestFocus()
|
||||
val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(messageText, InputMethodManager.SHOW_FORCED)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
private fun validateInput() {
|
||||
if (!this::sendMenuItem.isInitialized) return
|
||||
sendMenuItem.isEnabled = messageText.text?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
private fun onSendClick() {
|
||||
val title = titleText.text.toString()
|
||||
val message = messageText.text.toString()
|
||||
val tagsString = tagsText.text.toString()
|
||||
val tags = if (tagsString.isNotEmpty()) {
|
||||
tagsString.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
progress.visibility = View.VISIBLE
|
||||
errorText.visibility = View.GONE
|
||||
errorImage.visibility = View.GONE
|
||||
enableView(false)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val user = repository.getUser(baseUrl)
|
||||
api.publish(
|
||||
baseUrl = baseUrl,
|
||||
topic = topic,
|
||||
user = user,
|
||||
message = message,
|
||||
title = title,
|
||||
priority = selectedPriority,
|
||||
tags = tags,
|
||||
delay = ""
|
||||
)
|
||||
val activity = activity ?: return@launch
|
||||
activity.runOnUiThread {
|
||||
Toast.makeText(activity, R.string.publish_dialog_message_published, Toast.LENGTH_SHORT).show()
|
||||
publishListener?.onPublished()
|
||||
dismiss()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to publish message", e)
|
||||
val activity = activity ?: return@launch
|
||||
activity.runOnUiThread {
|
||||
progress.visibility = View.GONE
|
||||
val errorMessage = when (e) {
|
||||
is ApiService.UnauthorizedException -> {
|
||||
if (e.user != null) {
|
||||
getString(R.string.detail_test_message_error_unauthorized_user, e.user.username)
|
||||
} else {
|
||||
getString(R.string.detail_test_message_error_unauthorized_anon)
|
||||
}
|
||||
}
|
||||
is ApiService.EntityTooLargeException -> {
|
||||
getString(R.string.detail_test_message_error_too_large)
|
||||
}
|
||||
else -> {
|
||||
getString(R.string.publish_dialog_error_sending, e.message)
|
||||
}
|
||||
}
|
||||
errorText.text = errorMessage
|
||||
errorText.visibility = View.VISIBLE
|
||||
errorImage.visibility = View.VISIBLE
|
||||
enableView(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableView(enable: Boolean) {
|
||||
titleText.isEnabled = enable
|
||||
messageText.isEnabled = enable
|
||||
tagsText.isEnabled = enable
|
||||
priorityDropdown.isEnabled = enable
|
||||
sendMenuItem.isEnabled = enable && messageText.text?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NtfyPublishFragment"
|
||||
private const val ARG_BASE_URL = "baseUrl"
|
||||
private const val ARG_TOPIC = "topic"
|
||||
private const val ARG_MESSAGE = "message"
|
||||
|
||||
fun newInstance(baseUrl: String, topic: String, message: String = ""): PublishFragment {
|
||||
val fragment = PublishFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putString(ARG_BASE_URL, baseUrl)
|
||||
putString(ARG_TOPIC, topic)
|
||||
putString(ARG_MESSAGE, message)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -377,6 +377,26 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
dynamicColorsEnabled?.isVisible = true
|
||||
}
|
||||
|
||||
// Message bar enabled
|
||||
val messageBarEnabledPrefId = context?.getString(R.string.settings_general_message_bar_key) ?: return
|
||||
val messageBarEnabled: SwitchPreferenceCompat? = findPreference(messageBarEnabledPrefId)
|
||||
messageBarEnabled?.isChecked = repository.getMessageBarEnabled()
|
||||
messageBarEnabled?.preferenceDataStore = object : PreferenceDataStore() {
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
repository.setMessageBarEnabled(value)
|
||||
}
|
||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||
return repository.getMessageBarEnabled()
|
||||
}
|
||||
}
|
||||
messageBarEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreferenceCompat> { pref ->
|
||||
if (pref.isChecked) {
|
||||
getString(R.string.settings_general_message_bar_summary_enabled)
|
||||
} else {
|
||||
getString(R.string.settings_general_message_bar_summary_disabled)
|
||||
}
|
||||
}
|
||||
|
||||
// Default Base URL
|
||||
val appBaseUrl = getString(R.string.app_base_url)
|
||||
val defaultBaseUrlPrefId = context?.getString(R.string.settings_general_default_base_url_key) ?: return
|
||||
|
|
|
|||
10
app/src/main/res/drawable/ic_create_white_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_create_white_24dp.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
|
||||
10
app/src/main/res/drawable/ic_expand_less_gray_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_expand_less_gray_24dp.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"
|
||||
android:fillColor="#808080"/>
|
||||
</vector>
|
||||
|
||||
10
app/src/main/res/drawable/ic_send_gray_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_send_gray_24dp.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4.01,6.03l7.51,3.22 -7.52,-1 0.01,-2.22m7.5,8.72L4,17.97v-2.22l7.51,-1M2.01,3L2,10l15,2 -15,2 0.01,7L23,12 2.01,3z"
|
||||
android:fillColor="#808080"/>
|
||||
</vector>
|
||||
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
android:id="@+id/detail_content_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@color/detail_activity_background"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
|
|
@ -26,8 +25,12 @@
|
|||
style="@style/CardViewBackground"
|
||||
android:id="@+id/detail_notification_list_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/detail_message_bar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/detail_notification_list"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -45,8 +48,10 @@
|
|||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_no_notifications" app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
|
||||
android:id="@+id/detail_no_notifications"
|
||||
app:layout_constraintBottom_toTopOf="@id/detail_message_bar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -88,6 +93,28 @@
|
|||
android:autoLink="web"/>
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/detail_message_bar"
|
||||
layout="@layout/view_message_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/detail_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="24dp"
|
||||
android:contentDescription="@string/detail_fab_publish_description"
|
||||
android:src="@drawable/ic_create_white_24dp"
|
||||
android:visibility="gone"
|
||||
app:layout_anchor="@id/detail_content_layout"
|
||||
app:layout_anchorGravity="bottom|end"
|
||||
style="@style/FloatingActionButton"/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
|||
168
app/src/main/res/layout/fragment_publish_dialog.xml
Normal file
168
app/src/main/res/layout/fragment_publish_dialog.xml
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/publish_dialog_app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
app:elevation="0dp"
|
||||
app:liftOnScroll="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/publish_dialog_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorSurface"
|
||||
app:navigationIcon="@drawable/ic_close_white_24dp"
|
||||
app:navigationIconTint="?attr/colorOnSurface"
|
||||
app:titleTextColor="?attr/colorOnSurface"
|
||||
app:menu="@menu/menu_publish_dialog" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="?dialogPreferredPadding"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/publish_dialog_scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/publish_dialog_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:indeterminate="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/publish_dialog_title_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/publish_dialog_title_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/publish_dialog_title_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLines="1"
|
||||
android:inputType="text"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/publish_dialog_message_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/publish_dialog_title_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="10dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/publish_dialog_message_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/publish_dialog_message_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:minLines="3"
|
||||
android:gravity="start|top"
|
||||
android:inputType="textMultiLine|textCapSentences"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/publish_dialog_tags_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/publish_dialog_message_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="10dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/publish_dialog_tags_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/publish_dialog_tags_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLines="1"
|
||||
android:inputType="text"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:id="@+id/publish_dialog_priority_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/publish_dialog_tags_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/publish_dialog_priority_dropdown"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/publish_dialog_priority_default"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/publish_dialog_error_image"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginTop="1dp"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_error_red_24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/publish_dialog_error_text"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/publish_dialog_error_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/DangerText"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/publish_dialog_priority_layout"
|
||||
app:layout_constraintStart_toEndOf="@id/publish_dialog_error_image"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
66
app/src/main/res/layout/view_message_bar.xml
Normal file
66
app/src/main/res/layout/view_message_bar.xml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:elevation="4dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/message_bar_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorOutline"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/message_bar_expand_button"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_expand_less_gray_24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/message_bar_expand_button_description"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_bar_divider"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:tint="?attr/colorOnSurfaceVariant"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/message_bar_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/message_bar_hint"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:minHeight="40dp"
|
||||
android:maxLines="4"
|
||||
android:background="@null"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:importantForAutofill="no"
|
||||
app:layout_constraintStart_toEndOf="@id/message_bar_expand_button"
|
||||
app:layout_constraintEnd_toStartOf="@id/message_bar_send_button"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_bar_divider"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/message_bar_send_button"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_send_gray_24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/message_bar_publish_button_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/message_bar_divider"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:tint="?attr/colorPrimary"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
10
app/src/main/res/menu/menu_publish_dialog.xml
Normal file
10
app/src/main/res/menu/menu_publish_dialog.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/publish_dialog_send_button"
|
||||
android:title="@string/publish_dialog_button_send"
|
||||
android:enabled="false"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
||||
|
||||
5
app/src/main/res/values/dimens.xml
Normal file
5
app/src/main/res/values/dimens.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="fab_margin">24dp</dimen>
|
||||
</resources>
|
||||
|
||||
|
|
@ -199,6 +199,29 @@
|
|||
<string name="detail_settings_title">Subscription settings</string>
|
||||
<!-- ... -->
|
||||
|
||||
<!-- Publish dialog -->
|
||||
<string name="publish_dialog_title">Publish to %1$s</string>
|
||||
<string name="publish_dialog_title_hint">Title (optional)</string>
|
||||
<string name="publish_dialog_message_hint">Message</string>
|
||||
<string name="publish_dialog_tags_hint">Tags (optional, comma-separated)</string>
|
||||
<string name="publish_dialog_priority_default">Default priority</string>
|
||||
<string name="publish_dialog_priority_min">Min priority</string>
|
||||
<string name="publish_dialog_priority_low">Low priority</string>
|
||||
<string name="publish_dialog_priority_high">High priority</string>
|
||||
<string name="publish_dialog_priority_max">Max priority</string>
|
||||
<string name="publish_dialog_button_cancel">Cancel</string>
|
||||
<string name="publish_dialog_button_send">Send</string>
|
||||
<string name="publish_dialog_error_sending">Cannot send message: %1$s</string>
|
||||
<string name="publish_dialog_message_published">Message published</string>
|
||||
|
||||
<!-- Message bar -->
|
||||
<string name="message_bar_hint">Type a message here</string>
|
||||
<string name="message_bar_publish_button_description">Publish message</string>
|
||||
<string name="message_bar_expand_button_description">More options</string>
|
||||
|
||||
<!-- Detail activity: Publish FAB -->
|
||||
<string name="detail_fab_publish_description">Publish notification</string>
|
||||
|
||||
<!-- Share activity -->
|
||||
<string name="share_title">Share</string>
|
||||
<string name="share_menu_send">Share</string>
|
||||
|
|
@ -312,6 +335,9 @@
|
|||
<string name="settings_general_dynamic_colors_title">Dynamic colors</string>
|
||||
<string name="settings_general_dynamic_colors_summary_enabled">Using the dynamic system colors</string>
|
||||
<string name="settings_general_dynamic_colors_summary_disabled">Using the ntfy theme colors</string>
|
||||
<string name="settings_general_message_bar_title">Show message bar</string>
|
||||
<string name="settings_general_message_bar_summary_enabled">Message bar shown at bottom of topic view</string>
|
||||
<string name="settings_general_message_bar_summary_disabled">Publish button shown at bottom of topic view</string>
|
||||
<string name="settings_backup_restore_header">Backup & Restore</string>
|
||||
<string name="settings_backup_restore_backup_title">Back up to file</string>
|
||||
<string name="settings_backup_restore_backup_summary">Export config, notifications, and users</string>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
<string name="settings_general_users_key" translatable="false">ManageUsers</string>
|
||||
<string name="settings_general_dark_mode_key" translatable="false">DarkMode</string>
|
||||
<string name="settings_general_dynamic_colors_key" translatable="false">DynamicColors</string>
|
||||
<string name="settings_general_message_bar_key" translatable="false">MessageBarEnabled</string>
|
||||
<string name="settings_backup_restore_backup_key" translatable="false">Backup</string>
|
||||
<string name="settings_backup_restore_restore_key" translatable="false">Restore</string>
|
||||
<string name="settings_advanced_broadcast_key" translatable="false">BroadcastEnabled</string>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@
|
|||
app:key="@string/settings_general_dynamic_colors_key"
|
||||
app:title="@string/settings_general_dynamic_colors_title"
|
||||
app:isPreferenceVisible="false"/>
|
||||
<SwitchPreferenceCompat
|
||||
app:key="@string/settings_general_message_bar_key"
|
||||
app:title="@string/settings_general_message_bar_title"
|
||||
app:defaultValue="false"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory app:title="@string/settings_backup_restore_header">
|
||||
<ListPreference
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue