Make better

This commit is contained in:
Philipp Heckel 2026-01-24 20:27:43 -05:00
parent c2396dd1a3
commit 7cfef864a2
5 changed files with 38 additions and 73 deletions

View file

@ -574,6 +574,19 @@ interface NotificationDao {
@Query("SELECT * FROM notification WHERE subscriptionId = :subscriptionId AND deleted != 1 ORDER BY timestamp DESC")
fun listFlow(subscriptionId: Long): Flow<List<Notification>>
@Query("""
SELECT * FROM notification
WHERE subscriptionId = :subscriptionId
AND deleted != 1
AND (
title LIKE '%' || :query || '%' COLLATE NOCASE
OR message LIKE '%' || :query || '%' COLLATE NOCASE
OR tags LIKE '%' || :query || '%' COLLATE NOCASE
)
ORDER BY timestamp DESC
""")
fun listFlowFiltered(subscriptionId: Long, query: String): Flow<List<Notification>>
@Query("SELECT * FROM notification WHERE deleted = 1 AND attachment_contentUri <> ''")
fun listDeletedWithAttachments(): List<Notification>

View file

@ -120,6 +120,10 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
return notificationDao.listFlow(subscriptionId).asLiveData()
}
fun getNotificationsFilteredLiveData(subscriptionId: Long, query: String): LiveData<List<Notification>> {
return notificationDao.listFlowFiltered(subscriptionId, query).asLiveData()
}
fun getNotification(notificationId: String): Notification? {
return notificationDao.get(notificationId)
}

View file

@ -98,6 +98,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
// Search state
private var searchView: SearchView? = null
private var isSearchActive: Boolean = false
private lateinit var toolbar: com.google.android.material.appbar.MaterialToolbar
private var toolbarTextColor: Int = 0
// Action mode stuff
private var actionMode: ActionMode? = null
@ -147,13 +149,17 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
val dynamicColors = repository.getDynamicColorsEnabled()
val darkMode = isDarkThemeOn(this)
val statusBarColor = Colors.statusBarNormal(this, dynamicColors, darkMode)
val toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode)
toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode)
toolbarLayout.setBackgroundColor(statusBarColor)
val toolbar = toolbarLayout.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.toolbar)
toolbar = toolbarLayout.findViewById(R.id.toolbar)
toolbar.setTitleTextColor(toolbarTextColor)
toolbar.setNavigationIconTint(toolbarTextColor)
toolbar.overflowIcon?.setTint(toolbarTextColor)
// Set collapse icon (back arrow when search is expanded) with proper tint
val collapseIcon = ContextCompat.getDrawable(this, R.drawable.ic_arrow_back_white_24dp)?.mutate()
collapseIcon?.setTint(toolbarTextColor)
toolbar.collapseIcon = collapseIcon
setSupportActionBar(toolbar)
// Set system status bar appearance
@ -537,7 +543,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
subscriptionMutedUntil = subscription.mutedUntil
subscriptionDisplayName = displayName(appBaseUrl, subscription)
showHideInstantMenuItems(subscriptionInstant)
showHideMutedUntilMenuItems(subscriptionMutedUntil)
showHideCopyMenuItems(subscription.baseUrl)
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
@ -574,7 +579,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
this.menu = menu
// Tint menu icons based on theme
val toolbarTextColor = Colors.toolbarTextColor(this, repository.getDynamicColorsEnabled(), isDarkThemeOn(this))
for (i in 0 until menu.size) {
menu[i].icon?.setTint(toolbarTextColor)
}
@ -584,6 +588,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
searchView = searchItem?.actionView as? SearchView
searchView?.let { sv ->
sv.queryHint = getString(R.string.detail_search_hint)
sv.maxWidth = Integer.MAX_VALUE // Make SearchView expand fully
// Tint SearchView icons and text
val searchIcon = sv.findViewById<ImageView>(androidx.appcompat.R.id.search_button)
@ -612,6 +617,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
isSearchActive = true
// Hide other menu items when search is expanded
setMenuItemsVisibility(false)
// Ensure collapse icon is tinted (back arrow when search is expanded)
toolbar.collapseIcon?.setTint(toolbarTextColor)
return true
}
@ -621,7 +628,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
viewModel.setSearchQuery("")
// Restore menu items visibility
setMenuItemsVisibility(true)
showHideInstantMenuItems(subscriptionInstant)
showHideMutedUntilMenuItems(subscriptionMutedUntil)
showHideCopyMenuItems(subscriptionBaseUrl)
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
@ -631,7 +637,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
}
// Show and hide buttons
showHideInstantMenuItems(subscriptionInstant)
showHideMutedUntilMenuItems(subscriptionMutedUntil)
showHideCopyMenuItems(subscriptionBaseUrl)
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
@ -692,14 +697,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
onMutedUntilClick(enable = true)
true
}
R.id.detail_menu_enable_instant -> {
onInstantEnableClick(enable = true)
true
}
R.id.detail_menu_disable_instant -> {
onInstantEnableClick(enable = false)
true
}
R.id.detail_menu_connection_error -> {
onConnectionErrorClick()
true
@ -833,46 +830,6 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
}
}
private fun onInstantEnableClick(enable: Boolean) {
Log.d(TAG, "Toggling instant delivery setting for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
lifecycleScope.launch(Dispatchers.IO) {
val subscription = repository.getSubscription(subscriptionId)
val newSubscription = subscription?.copy(instant = enable)
newSubscription?.let { repository.updateSubscription(newSubscription) }
showHideInstantMenuItems(enable)
runOnUiThread {
if (enable) {
Toast.makeText(this@DetailActivity, getString(R.string.detail_instant_delivery_enabled), Toast.LENGTH_SHORT)
.show()
} else {
Toast.makeText(this@DetailActivity, getString(R.string.detail_instant_delivery_disabled), Toast.LENGTH_SHORT)
.show()
}
}
}
}
private fun showHideInstantMenuItems(enable: Boolean) {
if (!this::menu.isInitialized) {
return
}
subscriptionInstant = enable
runOnUiThread {
val appBaseUrl = getString(R.string.app_base_url)
val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
val allowToggleInstant = BuildConfig.FIREBASE_AVAILABLE && subscriptionBaseUrl == appBaseUrl
if (allowToggleInstant) {
enableInstantItem?.isVisible = !subscriptionInstant
disableInstantItem?.isVisible = subscriptionInstant
} else {
enableInstantItem?.isVisible = false
disableInstantItem?.isVisible = false
}
}
}
private fun showHideMutedUntilMenuItems(mutedUntilTimestamp: Long) {
if (!this::menu.isInitialized) {
return

View file

@ -4,10 +4,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.combineWith
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -15,6 +15,8 @@ class DetailViewModel(private val repository: Repository) : ViewModel() {
private val _searchQuery = MutableLiveData("")
val searchQuery: LiveData<String> = _searchQuery
private var currentSubscriptionId: Long = 0
fun setSearchQuery(query: String) {
_searchQuery.value = query
}
@ -24,19 +26,14 @@ class DetailViewModel(private val repository: Repository) : ViewModel() {
}
fun listFiltered(subscriptionId: Long): LiveData<List<Notification>> {
return repository.getNotificationsLiveData(subscriptionId)
.combineWith(_searchQuery) { notifications, query ->
if (query.isNullOrBlank()) {
notifications.orEmpty()
} else {
val q = query.lowercase()
notifications.orEmpty().filter { n ->
n.title.lowercase().contains(q) ||
n.message.lowercase().contains(q) ||
n.tags.lowercase().contains(q)
}
}
currentSubscriptionId = subscriptionId
return _searchQuery.switchMap { query ->
if (query.isNullOrBlank()) {
repository.getNotificationsLiveData(subscriptionId)
} else {
repository.getNotificationsFilteredLiveData(subscriptionId, query)
}
}
}
fun markAsDeleted(notificationId: String) = viewModelScope.launch(Dispatchers.IO) {

View file

@ -28,12 +28,6 @@
android:icon="@drawable/ic_notifications_off_white_outline_24dp"
android:title="@string/detail_menu_notifications_disabled_forever"
app:showAsAction="ifRoom" />
<item
android:id="@+id/detail_menu_enable_instant"
android:title="@string/detail_menu_enable_instant" />
<item
android:id="@+id/detail_menu_disable_instant"
android:title="@string/detail_menu_disable_instant" />
<item
android:id="@+id/detail_menu_settings"
android:title="@string/detail_menu_settings" />