Action mode delete for topics
This commit is contained in:
parent
f3268deeda
commit
b65bc749ab
12 changed files with 207 additions and 21 deletions
|
@ -10,8 +10,8 @@ data class Subscription(
|
|||
@PrimaryKey val id: Long, // Internal ID, only used in Repository and activities
|
||||
@ColumnInfo(name = "baseUrl") val baseUrl: String,
|
||||
@ColumnInfo(name = "topic") val topic: String,
|
||||
@ColumnInfo(name = "notifications") val notifications: Int,
|
||||
@ColumnInfo(name = "lastActive") val lastActive: Long // Unix timestamp
|
||||
@ColumnInfo(name = "notifications") val notifications: Int,
|
||||
@ColumnInfo(name = "lastActive") val lastActive: Long, // Unix timestamp
|
||||
)
|
||||
|
||||
@Entity
|
||||
|
|
|
@ -86,7 +86,6 @@ class AddFragment(private val viewModel: SubscriptionsViewModel, private val onS
|
|||
val topic = topicNameText.text.toString()
|
||||
val subscription = viewModel.get(baseUrl, topic)
|
||||
|
||||
println("sub $subscription")
|
||||
activity?.let {
|
||||
it.runOnUiThread {
|
||||
if (subscription != null) {
|
||||
|
|
|
@ -88,7 +88,7 @@ class DetailActivity : AppCompatActivity() {
|
|||
onTestClick()
|
||||
true
|
||||
}
|
||||
R.id.detail_menu_delete -> {
|
||||
R.id.detail_menu_unsubscribe -> {
|
||||
onDeleteClick()
|
||||
true
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
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
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.*
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import io.heckel.ntfy.R
|
||||
|
@ -18,30 +22,35 @@ import io.heckel.ntfy.data.topicShortUrl
|
|||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : AppCompatActivity(), ActionMode.Callback {
|
||||
private val viewModel by viewModels<SubscriptionsViewModel> {
|
||||
SubscriptionsViewModelFactory((application as Application).repository)
|
||||
}
|
||||
private lateinit var mainList: RecyclerView
|
||||
private lateinit var adapter: MainAdapter
|
||||
private lateinit var fab: View
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main_activity)
|
||||
|
||||
// TODO implement multi-select delete - https://enoent.fr/posts/recyclerview-basics/
|
||||
|
||||
// Action bar
|
||||
title = getString(R.string.main_action_bar_title)
|
||||
|
||||
// Floating action button ("+")
|
||||
val fab: View = findViewById(R.id.fab)
|
||||
fab = findViewById(R.id.fab)
|
||||
fab.setOnClickListener {
|
||||
onSubscribeButtonClick()
|
||||
}
|
||||
|
||||
// Update main list based on viewModel (& its datasource/livedata)
|
||||
val noEntries: View = findViewById(R.id.main_no_subscriptions)
|
||||
val adapter = SubscriptionsAdapter { subscription -> onSubscriptionItemClick(subscription) }
|
||||
val mainList: RecyclerView = findViewById(R.id.main_subscriptions_list)
|
||||
val onSubscriptionClick = { s: Subscription -> onSubscriptionItemClick(s) }
|
||||
val onSubscriptionLongClick = { s: Subscription -> onSubscriptionItemLongClick(s) }
|
||||
|
||||
mainList = findViewById(R.id.main_subscriptions_list)
|
||||
adapter = MainAdapter(onSubscriptionClick, onSubscriptionLongClick)
|
||||
mainList.adapter = adapter
|
||||
|
||||
viewModel.list().observe(this) {
|
||||
|
@ -85,7 +94,13 @@ class MainActivity : AppCompatActivity() {
|
|||
private fun onSubscribe(topic: String, baseUrl: String) {
|
||||
Log.d(TAG, "Adding subscription ${topicShortUrl(baseUrl, topic)}")
|
||||
|
||||
val subscription = Subscription(id = Random.nextLong(), baseUrl = baseUrl, topic = topic, notifications = 0, lastActive = Date().time/1000)
|
||||
val subscription = Subscription(
|
||||
id = Random.nextLong(),
|
||||
baseUrl = baseUrl,
|
||||
topic = topic,
|
||||
notifications = 0,
|
||||
lastActive = Date().time/1000
|
||||
)
|
||||
viewModel.add(subscription)
|
||||
FirebaseMessaging.getInstance().subscribeToTopic(topic) // FIXME ignores baseUrl
|
||||
|
||||
|
@ -93,6 +108,20 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun onSubscriptionItemClick(subscription: Subscription) {
|
||||
if (actionMode != null) {
|
||||
handleActionModeClick(subscription)
|
||||
} else {
|
||||
startDetailView(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSubscriptionItemLongClick(subscription: Subscription) {
|
||||
if (actionMode == null) {
|
||||
beginActionMode(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDetailView(subscription: Subscription) {
|
||||
Log.d(TAG, "Entering detail view for subscription $subscription")
|
||||
|
||||
val intent = Intent(this, DetailActivity::class.java)
|
||||
|
@ -115,11 +144,133 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleActionModeClick(subscription: Subscription) {
|
||||
adapter.toggleSelection(subscription.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.main_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.main_action_mode_unsubscribe -> {
|
||||
onMultiDeleteClick()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMultiDeleteClick() {
|
||||
Log.d(DetailActivity.TAG, "Showing multi-delete dialog for selected items")
|
||||
|
||||
val builder = AlertDialog.Builder(this)
|
||||
builder
|
||||
.setMessage(R.string.main_action_mode_delete_dialog_message)
|
||||
.setPositiveButton(R.string.main_action_mode_delete_dialog_permanently_delete) { _, _ ->
|
||||
adapter.selected.map { viewModel.remove(it) }
|
||||
finishActionMode()
|
||||
}
|
||||
.setNegativeButton(R.string.main_action_mode_delete_dialog_cancel) { _, _ ->
|
||||
finishActionMode()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
endActionModeAndRedraw()
|
||||
}
|
||||
|
||||
private fun beginActionMode(subscription: Subscription) {
|
||||
actionMode = startActionMode(this)
|
||||
adapter.selected.add(subscription.id)
|
||||
redrawList()
|
||||
|
||||
// Fade out FAB
|
||||
fab.alpha = 1f
|
||||
fab
|
||||
.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
fab.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
|
||||
// Fade status bar color
|
||||
val fromColor = ContextCompat.getColor(this, R.color.primaryColor)
|
||||
val toColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
|
||||
fadeStatusBarColor(fromColor, toColor)
|
||||
}
|
||||
|
||||
private fun finishActionMode() {
|
||||
actionMode!!.finish()
|
||||
endActionModeAndRedraw()
|
||||
}
|
||||
|
||||
private fun endActionModeAndRedraw() {
|
||||
actionMode = null
|
||||
adapter.selected.clear()
|
||||
redrawList()
|
||||
|
||||
// Fade in FAB
|
||||
fab.alpha = 0f
|
||||
fab.visibility = View.VISIBLE
|
||||
fab
|
||||
.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
fab.visibility = View.VISIBLE // Required to replace the old listener
|
||||
}
|
||||
})
|
||||
|
||||
// Fade status bar color
|
||||
val fromColor = ContextCompat.getColor(this, R.color.primaryDarkColor)
|
||||
val toColor = ContextCompat.getColor(this, R.color.primaryColor)
|
||||
fadeStatusBarColor(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"
|
||||
const val EXTRA_SUBSCRIPTION_BASE_URL = "subscriptionBaseUrl"
|
||||
const val EXTRA_SUBSCRIPTION_TOPIC = "subscriptionTopic"
|
||||
const val REQUEST_CODE_DELETE_SUBSCRIPTION = 1
|
||||
const val ANIMATION_DURATION = 80L
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,14 +12,16 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.data.topicShortUrl
|
||||
|
||||
class SubscriptionsAdapter(private val onClick: (Subscription) -> Unit) :
|
||||
ListAdapter<Subscription, SubscriptionsAdapter.SubscriptionViewHolder>(TopicDiffCallback) {
|
||||
|
||||
class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLongClick: (Subscription) -> Unit) :
|
||||
ListAdapter<Subscription, MainAdapter.SubscriptionViewHolder>(TopicDiffCallback) {
|
||||
val selected = mutableSetOf<Long>()
|
||||
|
||||
/* Creates and inflates view and return TopicViewHolder. */
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.main_fragment_item, parent, false)
|
||||
return SubscriptionViewHolder(view, onClick)
|
||||
return SubscriptionViewHolder(view, selected, onClick, onLongClick)
|
||||
}
|
||||
|
||||
/* Gets current topic and uses it to bind view. */
|
||||
|
@ -28,8 +30,16 @@ class SubscriptionsAdapter(private val onClick: (Subscription) -> Unit) :
|
|||
holder.bind(subscription)
|
||||
}
|
||||
|
||||
fun toggleSelection(subscriptionId: Long) {
|
||||
if (selected.contains(subscriptionId)) {
|
||||
selected.remove(subscriptionId)
|
||||
} else {
|
||||
selected.add(subscriptionId)
|
||||
}
|
||||
}
|
||||
|
||||
/* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */
|
||||
class SubscriptionViewHolder(itemView: View, val onClick: (Subscription) -> Unit) :
|
||||
class SubscriptionViewHolder(itemView: View, 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
|
||||
|
@ -46,6 +56,10 @@ class SubscriptionsAdapter(private val onClick: (Subscription) -> Unit) :
|
|||
nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
|
||||
statusView.text = statusMessage
|
||||
itemView.setOnClickListener { onClick(subscription) }
|
||||
itemView.setOnLongClickListener { onLongClick(subscription); true }
|
||||
if (selected.contains(subscription.id)) {
|
||||
itemView.setBackgroundResource(R.color.primarySelectedRowColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
10
app/src/main/res/drawable/baseline_delete_20.xml
Normal file
10
app/src/main/res/drawable/baseline_delete_20.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,6v10c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L16,6L4,6zM17,3h-3l-1,-1L7,2L6,3L3,3v2h14L17,3z"/>
|
||||
</vector>
|
|
@ -1,4 +1,4 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/detail_menu_test" android:title="@string/detail_menu_test"/>
|
||||
<item android:id="@+id/detail_menu_delete" android:title="@string/detail_menu_unsubscribe"/>
|
||||
<item android:id="@+id/detail_menu_unsubscribe" android:title="@string/detail_menu_unsubscribe"/>
|
||||
</menu>
|
||||
|
|
4
app/src/main/res/menu/main_action_mode_menu.xml
Normal file
4
app/src/main/res/menu/main_action_mode_menu.xml
Normal file
|
@ -0,0 +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"
|
||||
android:icon="@drawable/baseline_delete_20"/>
|
||||
</menu>
|
|
@ -2,9 +2,11 @@
|
|||
<resources>
|
||||
<color name="primaryColor">#338574</color>
|
||||
<color name="primaryLightColor">#338574</color>
|
||||
<color name="primaryDarkColor">#338574</color>
|
||||
<color name="primaryDarkColor">#2A6E60</color>
|
||||
|
||||
<color name="primaryTextColor">#000000</color>
|
||||
<color name="primaryLightTextColor">#FFFFFF</color>
|
||||
|
||||
<color name="primarySelectedRowColor">#EEEEEE</color>
|
||||
</resources>
|
||||
|
||||
|
|
|
@ -13,6 +13,11 @@
|
|||
<string name="main_menu_source_url">https://heckel.io/ntfy-android</string>
|
||||
<string name="main_menu_website_title">Visit ntfy.sh</string>
|
||||
|
||||
<!-- Main activity: Action mode -->
|
||||
<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>
|
||||
|
||||
<!-- Main activity: List and such -->
|
||||
<string name="main_item_status_connecting">connecting …</string>
|
||||
<string name="main_item_status_reconnecting">reconnecting …</string>
|
||||
|
@ -35,7 +40,7 @@
|
|||
<string name="detail_how_to_intro">To send notifications to this topic, simply PUT or POST to the topic URL.</string>
|
||||
<string name="detail_how_to_example"><![CDATA[ Example (using curl):<br/><tt>$ curl -d \"Hi\" %1$s</tt> ]]></string>
|
||||
<string name="detail_how_to_link">For more detailed instructions, check out the ntfy.sh website and documentation.</string>
|
||||
<string name="detail_delete_dialog_message">Do you really want to permanently delete this subscription and all its messages?</string>
|
||||
<string name="detail_delete_dialog_message">Do you really want to unsubscribe from this topic and delete all of the messages you received?</string>
|
||||
<string name="detail_delete_dialog_permanently_delete">Permanently delete</string>
|
||||
<string name="detail_delete_dialog_cancel">Cancel</string>
|
||||
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It was sent at %1$s.</string>
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
<item name="colorPrimaryVariant">@color/primaryDarkColor</item>
|
||||
<item name="colorAccent">@color/primaryLightColor</item>
|
||||
<item name="android:statusBarColor">@color/primaryColor</item>
|
||||
<item name="actionModeBackground">@color/primaryDarkColor</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue