Multi-delete notifications
This commit is contained in:
parent
b65bc749ab
commit
6c4a388c7e
11 changed files with 163 additions and 36 deletions
|
@ -70,8 +70,8 @@ interface NotificationDao {
|
|||
@Insert
|
||||
fun add(notification: Notification)
|
||||
|
||||
@Delete
|
||||
fun remove(notification: Notification)
|
||||
@Query("DELETE FROM notification WHERE id = :notificationId")
|
||||
fun remove(notificationId: String)
|
||||
|
||||
@Query("DELETE FROM notification WHERE subscriptionId = :subscriptionId")
|
||||
fun removeAll(subscriptionId: Long)
|
||||
|
|
|
@ -46,8 +46,8 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
@WorkerThread
|
||||
suspend fun removeNotification(notification: Notification) {
|
||||
notificationDao.remove(notification)
|
||||
suspend fun removeNotification(notificationId: String) {
|
||||
notificationDao.remove(notificationId)
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.util.Log
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
|
@ -12,6 +13,7 @@ import android.widget.TextView
|
|||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.volley.toolbox.StringRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
|
@ -23,14 +25,21 @@ import io.heckel.ntfy.data.topicUrl
|
|||
import java.util.*
|
||||
|
||||
|
||||
class DetailActivity : AppCompatActivity() {
|
||||
class DetailActivity : AppCompatActivity(), ActionMode.Callback {
|
||||
private val viewModel by viewModels<DetailViewModel> {
|
||||
DetailViewModelFactory((application as Application).repository)
|
||||
}
|
||||
|
||||
// Which subscription are we looking at
|
||||
private var subscriptionId: Long = 0L // Set in onCreate()
|
||||
private var subscriptionBaseUrl: String = "" // Set in onCreate()
|
||||
private var subscriptionTopic: String = "" // Set in onCreate()
|
||||
|
||||
// Action mode stuff
|
||||
private lateinit var mainList: RecyclerView
|
||||
private lateinit var adapter: DetailAdapter
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.detail_activity)
|
||||
|
@ -59,8 +68,11 @@ class DetailActivity : AppCompatActivity() {
|
|||
|
||||
// Update main list based on viewModel (& its datasource/livedata)
|
||||
val noEntriesText: View = findViewById(R.id.detail_no_notifications)
|
||||
val adapter = DetailAdapter { notification -> onNotificationClick(notification) }
|
||||
val mainList: RecyclerView = findViewById(R.id.detail_notification_list)
|
||||
val onNotificationClick = { n: Notification -> onNotificationClick(n) }
|
||||
val onNotificationLongClick = { n: Notification -> onNotificationLongClick(n) }
|
||||
|
||||
adapter = DetailAdapter(onNotificationClick, onNotificationLongClick)
|
||||
mainList = findViewById(R.id.detail_notification_list)
|
||||
mainList.adapter = adapter
|
||||
|
||||
viewModel.list(subscriptionId).observe(this) {
|
||||
|
@ -141,7 +153,100 @@ class DetailActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun onNotificationClick(notification: Notification) {
|
||||
// TODO Do something
|
||||
if (actionMode != null) {
|
||||
handleActionModeClick(notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNotificationLongClick(notification: Notification) {
|
||||
if (actionMode == null) {
|
||||
beginActionMode(notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleActionModeClick(notification: Notification) {
|
||||
adapter.toggleSelection(notification.id)
|
||||
if (adapter.selected.size == 0) {
|
||||
finishActionMode()
|
||||
} else {
|
||||
actionMode!!.title = adapter.selected.size.toString()
|
||||
redrawList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
this.actionMode = mode
|
||||
if (mode != null) {
|
||||
mode.menuInflater.inflate(R.menu.detail_action_mode_menu, menu)
|
||||
mode.title = "1" // One item selected
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
return when (item?.itemId) {
|
||||
R.id.detail_action_mode_delete -> {
|
||||
onMultiDeleteClick()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMultiDeleteClick() {
|
||||
Log.d(TAG, "Showing multi-delete dialog for selected items")
|
||||
|
||||
val builder = AlertDialog.Builder(this)
|
||||
builder
|
||||
.setMessage(R.string.detail_action_mode_delete_dialog_message)
|
||||
.setPositiveButton(R.string.detail_action_mode_delete_dialog_permanently_delete) { _, _ ->
|
||||
adapter.selected.map { viewModel.remove(it) }
|
||||
finishActionMode()
|
||||
}
|
||||
.setNegativeButton(R.string.detail_action_mode_delete_dialog_cancel) { _, _ ->
|
||||
finishActionMode()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
endActionModeAndRedraw()
|
||||
}
|
||||
|
||||
private fun beginActionMode(notification: Notification) {
|
||||
actionMode = startActionMode(this)
|
||||
adapter.selected.add(notification.id)
|
||||
redrawList()
|
||||
|
||||
// Fade status bar color
|
||||
val fromColor = ContextCompat.getColor(this, R.color.primaryColor)
|
||||
val toColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
|
||||
fadeStatusBarColor(window, fromColor, toColor)
|
||||
}
|
||||
|
||||
private fun finishActionMode() {
|
||||
actionMode!!.finish()
|
||||
endActionModeAndRedraw()
|
||||
}
|
||||
|
||||
private fun endActionModeAndRedraw() {
|
||||
actionMode = null
|
||||
adapter.selected.clear()
|
||||
redrawList()
|
||||
|
||||
// Fade status bar color
|
||||
val fromColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
|
||||
val toColor = ContextCompat.getColor(this, R.color.primaryColor)
|
||||
fadeStatusBarColor(window, fromColor, toColor)
|
||||
}
|
||||
|
||||
private fun redrawList() {
|
||||
mainList.adapter = adapter // Oh, what a hack ...
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -10,19 +9,17 @@ import androidx.recyclerview.widget.ListAdapter
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.data.topicShortUrl
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class DetailAdapter(private val onClick: (Notification) -> Unit) :
|
||||
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
||||
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
|
||||
val selected = mutableSetOf<String>() // Notification IDs
|
||||
|
||||
/* Creates and inflates view and return TopicViewHolder. */
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.detail_fragment_item, parent, false)
|
||||
return DetailViewHolder(view, onClick)
|
||||
return DetailViewHolder(view, selected, onClick, onLongClick)
|
||||
}
|
||||
|
||||
/* Gets current topic and uses it to bind view. */
|
||||
|
@ -30,8 +27,16 @@ class DetailAdapter(private val onClick: (Notification) -> Unit) :
|
|||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
fun toggleSelection(notificationId: String) {
|
||||
if (selected.contains(notificationId)) {
|
||||
selected.remove(notificationId)
|
||||
} else {
|
||||
selected.add(notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
/* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */
|
||||
class DetailViewHolder(itemView: View, val onClick: (Notification) -> Unit) :
|
||||
class DetailViewHolder(itemView: View, private val selected: Set<String>, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
private var notification: Notification? = null
|
||||
private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text)
|
||||
|
@ -42,6 +47,10 @@ class DetailAdapter(private val onClick: (Notification) -> Unit) :
|
|||
dateView.text = Date(notification.timestamp * 1000).toString()
|
||||
messageView.text = notification.message
|
||||
itemView.setOnClickListener { onClick(notification) }
|
||||
itemView.setOnLongClickListener { onLongClick(notification); true }
|
||||
if (selected.contains(notification.id)) {
|
||||
itemView.setBackgroundResource(R.color.primarySelectedRowColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ class DetailViewModel(private val repository: Repository) : ViewModel() {
|
|||
repository.addNotification(notification)
|
||||
}
|
||||
|
||||
fun remove(notification: Notification) = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.removeNotification(notification)
|
||||
fun remove(notificationId: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.removeNotification(notificationId)
|
||||
}
|
||||
|
||||
fun removeAll(subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) {
|
||||
|
|
|
@ -2,8 +2,6 @@ package io.heckel.ntfy.ui
|
|||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
|
@ -169,7 +167,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
return when (item?.itemId) {
|
||||
R.id.main_action_mode_unsubscribe -> {
|
||||
R.id.main_action_mode_delete -> {
|
||||
onMultiDeleteClick()
|
||||
true
|
||||
}
|
||||
|
@ -218,7 +216,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
// Fade status bar color
|
||||
val fromColor = ContextCompat.getColor(this, R.color.primaryColor)
|
||||
val toColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
|
||||
fadeStatusBarColor(fromColor, toColor)
|
||||
fadeStatusBarColor(window, fromColor, toColor)
|
||||
}
|
||||
|
||||
private fun finishActionMode() {
|
||||
|
@ -247,24 +245,13 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
|
|||
// Fade status bar color
|
||||
val fromColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
|
||||
val toColor = ContextCompat.getColor(this, R.color.primaryColor)
|
||||
fadeStatusBarColor(fromColor, toColor)
|
||||
|
||||
fadeStatusBarColor(window, fromColor, toColor)
|
||||
}
|
||||
|
||||
private fun redrawList() {
|
||||
mainList.adapter = adapter // Oh, what a hack ...
|
||||
}
|
||||
|
||||
private fun fadeStatusBarColor(fromColor: Int, toColor: Int) {
|
||||
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
|
||||
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
|
||||
statusBarColorAnimation.addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
window.statusBarColor = color
|
||||
}
|
||||
statusBarColorAnimation.start()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NtfyMainActivity"
|
||||
const val EXTRA_SUBSCRIPTION_ID = "subscriptionId"
|
||||
|
|
|
@ -15,7 +15,7 @@ import io.heckel.ntfy.data.topicShortUrl
|
|||
|
||||
class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLongClick: (Subscription) -> Unit) :
|
||||
ListAdapter<Subscription, MainAdapter.SubscriptionViewHolder>(TopicDiffCallback) {
|
||||
val selected = mutableSetOf<Long>()
|
||||
val selected = mutableSetOf<Long>() // Subscription IDs
|
||||
|
||||
/* Creates and inflates view and return TopicViewHolder. */
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
|
||||
|
@ -39,7 +39,7 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
|
|||
}
|
||||
|
||||
/* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */
|
||||
class SubscriptionViewHolder(itemView: View, val selected: Set<Long>, val onClick: (Subscription) -> Unit, val onLongClick: (Subscription) -> Unit) :
|
||||
class SubscriptionViewHolder(itemView: View, private val selected: Set<Long>, val onClick: (Subscription) -> Unit, val onLongClick: (Subscription) -> Unit) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
private var subscription: Subscription? = null
|
||||
private val context: Context = itemView.context
|
||||
|
|
15
app/src/main/java/io/heckel/ntfy/ui/Util.kt
Normal file
15
app/src/main/java/io/heckel/ntfy/ui/Util.kt
Normal file
|
@ -0,0 +1,15 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.Window
|
||||
|
||||
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
|
||||
fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
|
||||
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
|
||||
statusBarColorAnimation.addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
window.statusBarColor = color
|
||||
}
|
||||
statusBarColorAnimation.start()
|
||||
}
|
4
app/src/main/res/menu/detail_action_mode_menu.xml
Normal file
4
app/src/main/res/menu/detail_action_mode_menu.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/detail_action_mode_delete" android:title="@string/detail_action_mode_menu_delete"
|
||||
android:icon="@drawable/baseline_delete_20"/>
|
||||
</menu>
|
|
@ -1,4 +1,4 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/main_action_mode_unsubscribe" android:title="@string/detail_menu_unsubscribe"
|
||||
<item android:id="@+id/main_action_mode_delete" android:title="@string/main_action_mode_menu_unsubscribe"
|
||||
android:icon="@drawable/baseline_delete_20"/>
|
||||
</menu>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
<string name="main_menu_website_title">Visit ntfy.sh</string>
|
||||
|
||||
<!-- Main activity: Action mode -->
|
||||
<string name="main_action_mode_menu_unsubscribe">Unsubscribe</string>
|
||||
<string name="main_action_mode_delete_dialog_message">Do you really want to unsubscribe from selected topic(s) and permanently delete all the messages you received?</string>
|
||||
<string name="main_action_mode_delete_dialog_permanently_delete">Permanently delete</string>
|
||||
<string name="main_action_mode_delete_dialog_cancel">Cancel</string>
|
||||
|
@ -49,4 +50,10 @@
|
|||
<!-- Detail activity: Action bar -->
|
||||
<string name="detail_menu_test">Send test notification</string>
|
||||
<string name="detail_menu_unsubscribe">Unsubscribe</string>
|
||||
|
||||
<!-- Detail activity: Action mode -->
|
||||
<string name="detail_action_mode_menu_delete">Delete</string>
|
||||
<string name="detail_action_mode_delete_dialog_message">Do you really want to permanently delete the selected message(s)?</string>
|
||||
<string name="detail_action_mode_delete_dialog_permanently_delete">Permanently delete</string>
|
||||
<string name="detail_action_mode_delete_dialog_cancel">Cancel</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue