Muted until feature
This commit is contained in:
parent
71b5d56f6a
commit
8db05d7c88
46 changed files with 702 additions and 345 deletions
|
@ -58,7 +58,6 @@ dependencies {
|
|||
|
||||
// WorkManager
|
||||
implementation "androidx.work:work-runtime-ktx:2.6.0"
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
|
||||
// Room (SQLite)
|
||||
def roomVersion = "2.3.0"
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity android:name=".ui.SubscriptionSettingsActivity">
|
||||
</activity>
|
||||
<!-- Main activity -->
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
|
@ -32,22 +30,30 @@
|
|||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity> <!-- Detail activity -->
|
||||
</activity>
|
||||
|
||||
<!-- Detail activity -->
|
||||
<activity
|
||||
android:name=".ui.DetailActivity"
|
||||
android:parentActivityName=".ui.MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ui.MainActivity"/>
|
||||
</activity> <!-- Subscriber foreground service for hosts other than ntfy.sh -->
|
||||
<service android:name=".msg.SubscriberService"/> <!-- Subscriber service restart on reboot -->
|
||||
</activity>
|
||||
|
||||
<!-- Subscriber foreground service for hosts other than ntfy.sh -->
|
||||
<service android:name=".msg.SubscriberService"/>
|
||||
|
||||
<!-- Subscriber service restart on reboot -->
|
||||
<receiver
|
||||
android:name=".msg.SubscriberService$StartReceiver"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver> <!-- Firebase messaging -->
|
||||
</receiver>
|
||||
|
||||
<!-- Firebase messaging -->
|
||||
<service
|
||||
android:name=".msg.FirebaseService"
|
||||
android:exported="false">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.heckel.ntfy.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import io.heckel.ntfy.data.Database
|
||||
import io.heckel.ntfy.data.Repository
|
||||
|
@ -8,5 +9,8 @@ import io.heckel.ntfy.msg.ApiService
|
|||
|
||||
class Application : Application() {
|
||||
private val database by lazy { Database.getInstance(this) }
|
||||
val repository by lazy { Repository.getInstance(database.subscriptionDao(), database.notificationDao()) }
|
||||
val repository by lazy {
|
||||
val sharedPrefs = applicationContext.getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,7 @@ data class Subscription(
|
|||
@ColumnInfo(name = "baseUrl") val baseUrl: String,
|
||||
@ColumnInfo(name = "topic") val topic: String,
|
||||
@ColumnInfo(name = "instant") val instant: Boolean,
|
||||
@ColumnInfo(name = "mutedUntil") val mutedUntil: Long,
|
||||
//val notificationSchedule: String,
|
||||
//val notificationSound: String,
|
||||
@ColumnInfo(name = "mutedUntil") val mutedUntil: Long, // TODO notificationSound, notificationSchedule
|
||||
@Ignore val totalCount: Int = 0, // Total notifications
|
||||
@Ignore val newCount: Int = 0, // New notifications
|
||||
@Ignore val lastActive: Long = 0, // Unix timestamp
|
||||
|
@ -161,7 +159,7 @@ interface SubscriptionDao {
|
|||
@Dao
|
||||
interface NotificationDao {
|
||||
@Query("SELECT * FROM notification WHERE subscriptionId = :subscriptionId AND deleted != 1 ORDER BY timestamp DESC")
|
||||
fun list(subscriptionId: Long): Flow<List<Notification>>
|
||||
fun listFlow(subscriptionId: Long): Flow<List<Notification>>
|
||||
|
||||
@Query("SELECT id FROM notification WHERE subscriptionId = :subscriptionId") // Includes deleted
|
||||
fun listIds(subscriptionId: Long): List<String>
|
||||
|
@ -172,6 +170,9 @@ interface NotificationDao {
|
|||
@Query("SELECT * FROM notification WHERE id = :notificationId")
|
||||
fun get(notificationId: String): Notification?
|
||||
|
||||
@Query("UPDATE notification SET notificationId = 0 WHERE subscriptionId = :subscriptionId")
|
||||
fun clearAllNotificationIds(subscriptionId: Long)
|
||||
|
||||
@Update
|
||||
fun update(notification: Notification)
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package io.heckel.ntfy.data
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class Repository(private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) {
|
||||
class Repository(private val sharedPrefs: SharedPreferences, private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) {
|
||||
private val connectionStates = ConcurrentHashMap<Long, ConnectionState>()
|
||||
private val connectionStatesLiveData = MutableLiveData(connectionStates)
|
||||
val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ...
|
||||
|
@ -66,7 +67,11 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
}
|
||||
|
||||
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
|
||||
return notificationDao.list(subscriptionId).asLiveData()
|
||||
return notificationDao.listFlow(subscriptionId).asLiveData()
|
||||
}
|
||||
|
||||
fun clearAllNotificationIds(subscriptionId: Long) {
|
||||
return notificationDao.clearAllNotificationIds(subscriptionId)
|
||||
}
|
||||
|
||||
fun getNotification(notificationId: String): Notification? {
|
||||
|
@ -84,11 +89,17 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
val maybeExistingNotification = notificationDao.get(notification.id)
|
||||
if (maybeExistingNotification == null) {
|
||||
notificationDao.add(notification)
|
||||
return true
|
||||
return shouldNotify(notification)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun shouldNotify(notification: Notification): Boolean {
|
||||
val detailViewOpen = detailViewSubscriptionId.get() == notification.subscriptionId
|
||||
val muted = isMuted(notification.subscriptionId)
|
||||
return !detailViewOpen && !muted
|
||||
}
|
||||
|
||||
fun updateNotification(notification: Notification) {
|
||||
notificationDao.update(notification)
|
||||
}
|
||||
|
@ -105,6 +116,51 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
notificationDao.removeAll(subscriptionId)
|
||||
}
|
||||
|
||||
fun getPollWorkerVersion(): Int {
|
||||
return sharedPrefs.getInt(SHARED_PREFS_POLL_WORKER_VERSION, 0)
|
||||
}
|
||||
|
||||
fun setPollWorkerVersion(version: Int) {
|
||||
sharedPrefs.edit()
|
||||
.putInt(SHARED_PREFS_POLL_WORKER_VERSION, version)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private suspend fun isMuted(subscriptionId: Long): Boolean {
|
||||
if (isGlobalMuted()) {
|
||||
return true
|
||||
}
|
||||
val s = getSubscription(subscriptionId) ?: return true
|
||||
return s.mutedUntil == 1L || (s.mutedUntil > 1L && s.mutedUntil > System.currentTimeMillis()/1000)
|
||||
}
|
||||
|
||||
private fun isGlobalMuted(): Boolean {
|
||||
val mutedUntil = getGlobalMutedUntil()
|
||||
return mutedUntil == 1L || (mutedUntil > 1L && mutedUntil > System.currentTimeMillis()/1000)
|
||||
}
|
||||
|
||||
fun getGlobalMutedUntil(): Long {
|
||||
return sharedPrefs.getLong(SHARED_PREFS_MUTED_UNTIL_TIMESTAMP, 0L)
|
||||
}
|
||||
|
||||
fun setGlobalMutedUntil(mutedUntilTimestamp: Long) {
|
||||
sharedPrefs.edit()
|
||||
.putLong(SHARED_PREFS_MUTED_UNTIL_TIMESTAMP, mutedUntilTimestamp)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun checkGlobalMutedUntil(): Boolean {
|
||||
val mutedUntil = sharedPrefs.getLong(SHARED_PREFS_MUTED_UNTIL_TIMESTAMP, 0L)
|
||||
val expired = mutedUntil > 1L && System.currentTimeMillis()/1000 > mutedUntil
|
||||
if (expired) {
|
||||
sharedPrefs.edit()
|
||||
.putLong(SHARED_PREFS_MUTED_UNTIL_TIMESTAMP, 0L)
|
||||
.apply()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun toSubscriptionList(list: List<SubscriptionWithMetadata>): List<Subscription> {
|
||||
return list.map { s ->
|
||||
val connectionState = connectionStates.getOrElse(s.id) { ConnectionState.NOT_APPLICABLE }
|
||||
|
@ -162,12 +218,16 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val SHARED_PREFS_ID = "MainPreferences"
|
||||
const val SHARED_PREFS_POLL_WORKER_VERSION = "PollWorkerVersion"
|
||||
const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil"
|
||||
|
||||
private const val TAG = "NtfyRepository"
|
||||
private var instance: Repository? = null
|
||||
|
||||
fun getInstance(subscriptionDao: SubscriptionDao, notificationDao: NotificationDao): Repository {
|
||||
fun getInstance(sharedPrefs: SharedPreferences, subscriptionDao: SubscriptionDao, notificationDao: NotificationDao): Repository {
|
||||
return synchronized(Repository::class) {
|
||||
val newInstance = instance ?: Repository(subscriptionDao, notificationDao)
|
||||
val newInstance = instance ?: Repository(sharedPrefs, subscriptionDao, notificationDao)
|
||||
instance = newInstance
|
||||
newInstance
|
||||
}
|
||||
|
|
|
@ -7,3 +7,4 @@ fun topicShortUrl(baseUrl: String, topic: String) =
|
|||
topicUrl(baseUrl, topic)
|
||||
.replace("http://", "")
|
||||
.replace("https://", "")
|
||||
|
||||
|
|
|
@ -48,11 +48,10 @@ class FirebaseService : FirebaseMessagingService() {
|
|||
notificationId = Random.nextInt(),
|
||||
deleted = false
|
||||
)
|
||||
val added = repository.addNotification(notification)
|
||||
val detailViewOpen = repository.detailViewSubscriptionId.get() == subscription.id
|
||||
val shouldNotify = repository.addNotification(notification)
|
||||
|
||||
// Send notification (only if it's not already known)
|
||||
if (added && !detailViewOpen) {
|
||||
if (shouldNotify) {
|
||||
Log.d(TAG, "Sending notification for message: from=${remoteMessage.from}, data=${data}")
|
||||
notifier.send(subscription, notification)
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ class NotificationService(val context: Context) {
|
|||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
|
||||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
|
||||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
|
||||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
|
||||
val pendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
|
||||
addNextIntentWithParentStack(intent) // Add the intent, which inflates the back stack
|
||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // Get the PendingIntent containing the entire back stack
|
||||
|
|
|
@ -174,10 +174,8 @@ class SubscriberService : Service() {
|
|||
val url = topicUrl(subscription.baseUrl, subscription.topic)
|
||||
Log.d(TAG, "[$url] Received notification: $n")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val added = repository.addNotification(n)
|
||||
val detailViewOpen = repository.detailViewSubscriptionId.get() == subscription.id
|
||||
|
||||
if (added && !detailViewOpen) {
|
||||
val shouldNotify = repository.addNotification(n)
|
||||
if (shouldNotify) {
|
||||
Log.d(TAG, "[$url] Showing notification: $n")
|
||||
notifier.send(subscription, n)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.content.Context
|
|||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
|
@ -47,11 +46,12 @@ class AddFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
// Dependencies
|
||||
val database = Database.getInstance(activity!!.applicationContext)
|
||||
repository = Repository.getInstance(database.subscriptionDao(), database.notificationDao())
|
||||
val database = Database.getInstance(requireActivity().applicationContext)
|
||||
val sharedPrefs = requireActivity().getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
repository = Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
|
||||
|
||||
// Build root view
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.add_dialog_fragment, null)
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_add_dialog, null)
|
||||
topicNameText = view.findViewById(R.id.add_dialog_topic_text) as TextInputEditText
|
||||
baseUrlText = view.findViewById(R.id.add_dialog_base_url_text) as TextInputEditText
|
||||
instantDeliveryBox = view.findViewById(R.id.add_dialog_instant_delivery_box)
|
||||
|
|
|
@ -27,11 +27,9 @@ import io.heckel.ntfy.data.topicShortUrl
|
|||
import io.heckel.ntfy.data.topicUrl
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import io.heckel.ntfy.msg.NotificationService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFragment.NotificationSettingsListener {
|
||||
private val viewModel by viewModels<DetailViewModel> {
|
||||
|
@ -47,6 +45,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
private var subscriptionBaseUrl: String = "" // Set in onCreate()
|
||||
private var subscriptionTopic: String = "" // Set in onCreate()
|
||||
private var subscriptionInstant: Boolean = false // Set in onCreate() & updated by options menu!
|
||||
private var subscriptionMutedUntil: Long = 0L // Set in onCreate() & updated by options menu!
|
||||
|
||||
// UI elements
|
||||
private lateinit var adapter: DetailAdapter
|
||||
|
@ -59,7 +58,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.detail_activity)
|
||||
setContentView(R.layout.activity_detail)
|
||||
|
||||
Log.d(MainActivity.TAG, "Create $this")
|
||||
|
||||
|
@ -75,6 +74,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
subscriptionBaseUrl = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return
|
||||
subscriptionTopic = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC) ?: return
|
||||
subscriptionInstant = intent.getBooleanExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, false)
|
||||
subscriptionMutedUntil = intent.getLongExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, 0L)
|
||||
|
||||
// Set title
|
||||
val subscriptionBaseUrl = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return
|
||||
|
@ -152,14 +152,14 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.d(TAG, "onResume hook: Marking subscription $subscriptionId as 'not open'")
|
||||
repository.detailViewSubscriptionId.set(0) // Mark as closed
|
||||
Log.d(TAG, "onPause hook: Removing 'notificationId' from all notifications for $subscriptionId")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
// Note: This is here and not in onDestroy/onStop, because we want to clear notifications as early
|
||||
// as possible, so that we don't see the "new" bubble in the main list anymore.
|
||||
repository.clearAllNotificationIds(subscriptionId)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "onPause hook: Marking subscription $subscriptionId as 'not open'")
|
||||
repository.detailViewSubscriptionId.set(0) // Mark as closed
|
||||
Log.d(TAG, "onDestroy hook: Marking subscription $subscriptionId as 'not open'")
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun maybeCancelNotificationPopups(notifications: List<Notification>) {
|
||||
|
@ -168,27 +168,60 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
notificationsWithPopups.forEach { notification ->
|
||||
notifier?.cancel(notification)
|
||||
repository.updateNotification(notification.copy(notificationId = 0))
|
||||
// Do NOT remove the notificationId here, we need that for the UI indicators; we'll remove it in onPause()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.detail_action_bar_menu, menu)
|
||||
menuInflater.inflate(R.menu.menu_detail_action_bar, menu)
|
||||
this.menu = menu
|
||||
|
||||
// Show and hide buttons
|
||||
showHideInstantMenuItems(subscriptionInstant)
|
||||
showHideNotificationMenuItems(subscriptionMutedUntil)
|
||||
|
||||
// Regularly check if "notification muted" time has passed
|
||||
// NOTE: This is done here, because then we know that we've initialized the menu items.
|
||||
startNotificationMutedChecker()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun startNotificationMutedChecker() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
delay(1000) // Just to be sure we've initialized all the things, we wait a bit ...
|
||||
while (isActive) {
|
||||
Log.d(TAG, "Checking 'muted until' timestamp for subscription $subscriptionId")
|
||||
val subscription = repository.getSubscription(subscriptionId) ?: return@launch
|
||||
val mutedUntilExpired = subscription.mutedUntil > 1L && System.currentTimeMillis()/1000 > subscription.mutedUntil
|
||||
if (mutedUntilExpired) {
|
||||
val newSubscription = subscription.copy(mutedUntil = 0L)
|
||||
repository.updateSubscription(newSubscription)
|
||||
showHideNotificationMenuItems(0L)
|
||||
}
|
||||
delay(60_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.detail_menu_test -> {
|
||||
onTestClick()
|
||||
true
|
||||
}
|
||||
R.id.detail_menu_notification -> {
|
||||
onNotificationSettingsClick()
|
||||
R.id.detail_menu_notifications_enabled -> {
|
||||
onNotificationSettingsClick(enable = false)
|
||||
true
|
||||
}
|
||||
R.id.detail_menu_notifications_disabled_until -> {
|
||||
onNotificationSettingsClick(enable = true)
|
||||
true
|
||||
}
|
||||
R.id.detail_menu_notifications_disabled_forever -> {
|
||||
onNotificationSettingsClick(enable = true)
|
||||
true
|
||||
}
|
||||
R.id.detail_menu_enable_instant -> {
|
||||
|
@ -232,36 +265,35 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
}
|
||||
}
|
||||
|
||||
private fun onNotificationSettingsClick() {
|
||||
private fun onNotificationSettingsClick(enable: Boolean) {
|
||||
if (!enable) {
|
||||
Log.d(TAG, "Showing notification settings dialog for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
|
||||
val intent = Intent(this, SubscriptionSettingsActivity::class.java)
|
||||
startActivityForResult(intent, /*XXXXXX*/MainActivity.REQUEST_CODE_DELETE_SUBSCRIPTION)
|
||||
/*
|
||||
val notificationFragment = NotificationFragment()
|
||||
notificationFragment.show(supportFragmentManager, NotificationFragment.TAG)*/
|
||||
notificationFragment.show(supportFragmentManager, NotificationFragment.TAG)
|
||||
} else {
|
||||
Log.d(TAG, "Re-enabling notifications ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
|
||||
onNotificationMutedUntilChanged(0L)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNotificationSettingsChanged(mutedUntil: Long) {
|
||||
override fun onNotificationMutedUntilChanged(mutedUntilTimestamp: Long) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val subscription = repository.getSubscription(subscriptionId)
|
||||
val newSubscription = subscription?.copy(mutedUntil = mutedUntil)
|
||||
val newSubscription = subscription?.copy(mutedUntil = mutedUntilTimestamp)
|
||||
newSubscription?.let { repository.updateSubscription(newSubscription) }
|
||||
subscriptionMutedUntil = mutedUntilTimestamp
|
||||
showHideNotificationMenuItems(mutedUntilTimestamp)
|
||||
runOnUiThread {
|
||||
when (mutedUntil) {
|
||||
0L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_SHORT).show()
|
||||
1L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_SHORT).show()
|
||||
when (mutedUntilTimestamp) {
|
||||
0L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_LONG).show()
|
||||
1L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_LONG).show()
|
||||
else -> {
|
||||
val mutedUntilDate = Date(mutedUntil).toString()
|
||||
Toast.makeText(
|
||||
this@DetailActivity,
|
||||
getString(R.string.notification_dialog_muted_until_toast_message, mutedUntilDate),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
val formattedDate = formatDateShort(mutedUntilTimestamp)
|
||||
Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_muted_until_toast_message, formattedDate), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun onCopyUrlClick() {
|
||||
|
@ -352,6 +384,23 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
}
|
||||
}
|
||||
|
||||
private fun showHideNotificationMenuItems(mutedUntilTimestamp: Long) {
|
||||
subscriptionMutedUntil = mutedUntilTimestamp
|
||||
runOnUiThread {
|
||||
val notificationsEnabledItem = menu.findItem(R.id.detail_menu_notifications_enabled)
|
||||
val notificationsDisabledUntilItem = menu.findItem(R.id.detail_menu_notifications_disabled_until)
|
||||
val notificationsDisabledForeverItem = menu.findItem(R.id.detail_menu_notifications_disabled_forever)
|
||||
notificationsEnabledItem?.isVisible = subscriptionMutedUntil == 0L
|
||||
notificationsDisabledForeverItem?.isVisible = subscriptionMutedUntil == 1L
|
||||
notificationsDisabledUntilItem?.isVisible = subscriptionMutedUntil > 1L
|
||||
if (subscriptionMutedUntil > 1L) {
|
||||
val formattedDate = formatDateShort(subscriptionMutedUntil)
|
||||
notificationsDisabledUntilItem?.title = getString(R.string.detail_menu_notifications_disabled_until, formattedDate)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDeleteClick() {
|
||||
Log.d(TAG, "Deleting subscription ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
|
||||
|
||||
|
@ -365,6 +414,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscriptionBaseUrl)
|
||||
.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscriptionTopic)
|
||||
.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscriptionInstant)
|
||||
.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscriptionMutedUntil)
|
||||
setResult(RESULT_OK, result)
|
||||
finish()
|
||||
|
||||
|
@ -414,7 +464,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
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.menuInflater.inflate(R.menu.menu_detail_action_mode, menu)
|
||||
mode.title = "1" // One item selected
|
||||
}
|
||||
return true
|
||||
|
|
|
@ -18,7 +18,7 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
|||
/* 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)
|
||||
.inflate(R.layout.fragment_detail_item, parent, false)
|
||||
return DetailViewHolder(view, selected, onClick, onLongClick)
|
||||
}
|
||||
|
||||
|
@ -41,11 +41,13 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
|||
private var notification: Notification? = null
|
||||
private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text)
|
||||
private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text)
|
||||
private val newImageView: View = itemView.findViewById(R.id.detail_item_new)
|
||||
|
||||
fun bind(notification: Notification) {
|
||||
this.notification = notification
|
||||
dateView.text = Date(notification.timestamp * 1000).toString()
|
||||
messageView.text = notification.message
|
||||
newImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE
|
||||
itemView.setOnClickListener { onClick(notification) }
|
||||
itemView.setOnLongClickListener { onLongClick(notification); true }
|
||||
if (selected.contains(notification.id)) {
|
||||
|
|
|
@ -3,7 +3,6 @@ package io.heckel.ntfy.ui
|
|||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
|
@ -29,23 +28,28 @@ import io.heckel.ntfy.msg.ApiService
|
|||
import io.heckel.ntfy.msg.NotificationService
|
||||
import io.heckel.ntfy.work.PollWorker
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.SubscribeListener {
|
||||
class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.SubscribeListener, NotificationFragment.NotificationSettingsListener {
|
||||
private val viewModel by viewModels<SubscriptionsViewModel> {
|
||||
SubscriptionsViewModelFactory((application as Application).repository)
|
||||
}
|
||||
private val repository by lazy { (application as Application).repository }
|
||||
private val api = ApiService()
|
||||
|
||||
// UI elements
|
||||
private lateinit var menu: Menu
|
||||
private lateinit var mainList: RecyclerView
|
||||
private lateinit var mainListContainer: SwipeRefreshLayout
|
||||
private lateinit var adapter: MainAdapter
|
||||
private lateinit var fab: View
|
||||
|
||||
// Other stuff
|
||||
private var actionMode: ActionMode? = null
|
||||
private var workManager: WorkManager? = null // Context-dependent
|
||||
private var notifier: NotificationService? = null // Context-dependent
|
||||
|
@ -54,7 +58,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main_activity)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
Log.d(TAG, "Create $this")
|
||||
|
||||
|
@ -110,15 +114,13 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
}
|
||||
|
||||
private fun startPeriodicWorker() {
|
||||
val sharedPrefs = getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
val workPolicy = if (sharedPrefs.getInt(SHARED_PREFS_POLL_WORKER_VERSION, 0) == PollWorker.VERSION) {
|
||||
val pollWorkerVersion = repository.getPollWorkerVersion()
|
||||
val workPolicy = if (pollWorkerVersion == PollWorker.VERSION) {
|
||||
Log.d(TAG, "Poll worker version matches: choosing KEEP as existing work policy")
|
||||
ExistingPeriodicWorkPolicy.KEEP
|
||||
} else {
|
||||
Log.d(TAG, "Poll worker version DOES NOT MATCH: choosing REPLACE as existing work policy")
|
||||
sharedPrefs.edit()
|
||||
.putInt(SHARED_PREFS_POLL_WORKER_VERSION, PollWorker.VERSION)
|
||||
.apply()
|
||||
repository.setPollWorkerVersion(PollWorker.VERSION)
|
||||
ExistingPeriodicWorkPolicy.REPLACE
|
||||
}
|
||||
val constraints = Constraints.Builder()
|
||||
|
@ -133,12 +135,76 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.main_action_bar_menu, menu)
|
||||
menuInflater.inflate(R.menu.menu_main_action_bar, menu)
|
||||
this.menu = menu
|
||||
showHideNotificationMenuItems()
|
||||
startNotificationMutedChecker() // This is done here, because then we know that we've initialized the menu
|
||||
return true
|
||||
}
|
||||
|
||||
private fun startNotificationMutedChecker() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
delay(1000) // Just to be sure we've initialized all the things, we wait a bit ...
|
||||
while (isActive) {
|
||||
Log.d(DetailActivity.TAG, "Checking global and subscription-specific 'muted until' timestamp")
|
||||
|
||||
// Check global
|
||||
val changed = repository.checkGlobalMutedUntil()
|
||||
if (changed) {
|
||||
Log.d(TAG, "Global muted until timestamp expired; updating prefs")
|
||||
showHideNotificationMenuItems()
|
||||
}
|
||||
|
||||
// Check subscriptions
|
||||
var rerenderList = false
|
||||
repository.getSubscriptions().forEach { subscription ->
|
||||
val mutedUntilExpired = subscription.mutedUntil > 1L && System.currentTimeMillis()/1000 > subscription.mutedUntil
|
||||
if (mutedUntilExpired) {
|
||||
Log.d(TAG, "Subscription ${subscription.id}: Muted until timestamp expired, updating subscription")
|
||||
val newSubscription = subscription.copy(mutedUntil = 0L)
|
||||
repository.updateSubscription(newSubscription)
|
||||
rerenderList = true
|
||||
}
|
||||
}
|
||||
if (rerenderList) {
|
||||
redrawList()
|
||||
}
|
||||
|
||||
delay(60_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showHideNotificationMenuItems() {
|
||||
val mutedUntilSeconds = repository.getGlobalMutedUntil()
|
||||
runOnUiThread {
|
||||
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
|
||||
val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until)
|
||||
val notificationsDisabledForeverItem = menu.findItem(R.id.main_menu_notifications_disabled_forever)
|
||||
notificationsEnabledItem?.isVisible = mutedUntilSeconds == 0L
|
||||
notificationsDisabledForeverItem?.isVisible = mutedUntilSeconds == 1L
|
||||
notificationsDisabledUntilItem?.isVisible = mutedUntilSeconds > 1L
|
||||
if (mutedUntilSeconds > 1L) {
|
||||
val formattedDate = formatDateShort(mutedUntilSeconds)
|
||||
notificationsDisabledUntilItem?.title = getString(R.string.main_menu_notifications_disabled_until, formattedDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.main_menu_notifications_enabled -> {
|
||||
onNotificationSettingsClick(enable = false)
|
||||
true
|
||||
}
|
||||
R.id.main_menu_notifications_disabled_forever -> {
|
||||
onNotificationSettingsClick(enable = true)
|
||||
true
|
||||
}
|
||||
R.id.main_menu_notifications_disabled_until -> {
|
||||
onNotificationSettingsClick(enable = true)
|
||||
true
|
||||
}
|
||||
R.id.main_menu_source -> {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_source_url))))
|
||||
true
|
||||
|
@ -151,6 +217,32 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
}
|
||||
}
|
||||
|
||||
private fun onNotificationSettingsClick(enable: Boolean) {
|
||||
if (!enable) {
|
||||
Log.d(TAG, "Showing global notification settings dialog")
|
||||
val notificationFragment = NotificationFragment()
|
||||
notificationFragment.show(supportFragmentManager, NotificationFragment.TAG)
|
||||
} else {
|
||||
Log.d(TAG, "Re-enabling global notifications")
|
||||
onNotificationMutedUntilChanged(0L)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNotificationMutedUntilChanged(mutedUntilTimestamp: Long) {
|
||||
repository.setGlobalMutedUntil(mutedUntilTimestamp)
|
||||
showHideNotificationMenuItems()
|
||||
runOnUiThread {
|
||||
when (mutedUntilTimestamp) {
|
||||
0L -> Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_LONG).show()
|
||||
1L -> Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_LONG).show()
|
||||
else -> {
|
||||
val formattedDate = formatDateShort(mutedUntilTimestamp)
|
||||
Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_muted_until_toast_message, formattedDate), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSubscribeButtonClick() {
|
||||
val newFragment = AddFragment()
|
||||
newFragment.show(supportFragmentManager, AddFragment.TAG)
|
||||
|
@ -223,10 +315,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic)
|
||||
val newNotifications = repository.onlyNewNotifications(subscription.id, notifications)
|
||||
newNotifications.forEach { notification ->
|
||||
val notificationWithId = notification.copy(notificationId = Random.nextInt())
|
||||
repository.addNotification(notificationWithId)
|
||||
notifier?.send(subscription, notificationWithId)
|
||||
newNotificationsCount++
|
||||
val notificationWithId = notification.copy(notificationId = Random.nextInt())
|
||||
val shouldNotify = repository.addNotification(notificationWithId)
|
||||
if (shouldNotify) {
|
||||
notifier?.send(subscription, notificationWithId)
|
||||
}
|
||||
}
|
||||
}
|
||||
val toastMessage = if (newNotificationsCount == 0) {
|
||||
|
@ -257,6 +351,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
intent.putExtra(EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
|
||||
intent.putExtra(EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
|
||||
intent.putExtra(EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
|
||||
intent.putExtra(EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
|
||||
startActivityForResult(intent, REQUEST_CODE_DELETE_SUBSCRIPTION)
|
||||
}
|
||||
|
||||
|
@ -293,7 +388,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
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.menuInflater.inflate(R.menu.menu_main_action_mode, menu)
|
||||
mode.title = "1" // One item selected
|
||||
}
|
||||
return true
|
||||
|
@ -387,8 +482,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
}
|
||||
|
||||
private fun redrawList() {
|
||||
runOnUiThread {
|
||||
mainList.adapter = adapter // Oh, what a hack ...
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NtfyMainActivity"
|
||||
|
@ -396,9 +493,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
const val EXTRA_SUBSCRIPTION_BASE_URL = "subscriptionBaseUrl"
|
||||
const val EXTRA_SUBSCRIPTION_TOPIC = "subscriptionTopic"
|
||||
const val EXTRA_SUBSCRIPTION_INSTANT = "subscriptionInstant"
|
||||
const val EXTRA_SUBSCRIPTION_MUTED_UNTIL = "subscriptionMutedUntil"
|
||||
const val REQUEST_CODE_DELETE_SUBSCRIPTION = 1
|
||||
const val ANIMATION_DURATION = 80L
|
||||
const val SHARED_PREFS_ID = "MainPreferences"
|
||||
const val SHARED_PREFS_POLL_WORKER_VERSION = "PollWorkerVersion"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
|
|||
/* 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)
|
||||
.inflate(R.layout.fragment_main_item, parent, false)
|
||||
return SubscriptionViewHolder(view, selected, onClick, onLongClick)
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,8 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
|
|||
private val nameView: TextView = itemView.findViewById(R.id.main_item_text)
|
||||
private val statusView: TextView = itemView.findViewById(R.id.main_item_status)
|
||||
private val dateView: TextView = itemView.findViewById(R.id.main_item_date)
|
||||
private val notificationDisabledUntilImageView: View = itemView.findViewById(R.id.main_item_notification_disabled_until_image)
|
||||
private val notificationDisabledForeverImageView: View = itemView.findViewById(R.id.main_item_notification_disabled_forever_image)
|
||||
private val instantImageView: View = itemView.findViewById(R.id.main_item_instant_image)
|
||||
private val newItemsView: TextView = itemView.findViewById(R.id.main_item_new)
|
||||
|
||||
|
@ -78,11 +80,9 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
|
|||
nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
|
||||
statusView.text = statusMessage
|
||||
dateView.text = dateText
|
||||
if (subscription.instant) {
|
||||
instantImageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
instantImageView.visibility = View.GONE
|
||||
}
|
||||
notificationDisabledUntilImageView.visibility = if (subscription.mutedUntil > 1L) View.VISIBLE else View.GONE
|
||||
notificationDisabledForeverImageView.visibility = if (subscription.mutedUntil == 1L) View.VISIBLE else View.GONE
|
||||
instantImageView.visibility = if (subscription.instant) View.VISIBLE else View.GONE
|
||||
if (subscription.newCount > 0) {
|
||||
newItemsView.visibility = View.VISIBLE
|
||||
newItemsView.text = if (subscription.newCount <= 99) subscription.newCount.toString() else "99+"
|
||||
|
|
|
@ -4,27 +4,29 @@ import android.app.AlertDialog
|
|||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.RadioButton
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.data.Database
|
||||
import io.heckel.ntfy.data.Repository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class NotificationFragment : DialogFragment() {
|
||||
private lateinit var repository: Repository
|
||||
private lateinit var settingsListener: NotificationSettingsListener
|
||||
private lateinit var muteFor30minButton: RadioButton
|
||||
private lateinit var muteFor1hButton: RadioButton
|
||||
private lateinit var muteFor2hButton: RadioButton
|
||||
private lateinit var muteFor8hButton: RadioButton
|
||||
private lateinit var muteUntilTomorrowButton: RadioButton
|
||||
private lateinit var muteForeverButton: RadioButton
|
||||
|
||||
interface NotificationSettingsListener {
|
||||
fun onNotificationSettingsChanged(mutedUntil: Long)
|
||||
fun onNotificationMutedUntilChanged(mutedUntilTimestamp: Long)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
|
@ -38,34 +40,55 @@ class NotificationFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
// Dependencies
|
||||
val database = Database.getInstance(activity!!.applicationContext)
|
||||
repository = Repository.getInstance(database.subscriptionDao(), database.notificationDao())
|
||||
val database = Database.getInstance(requireActivity().applicationContext)
|
||||
val sharedPrefs = requireActivity().getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
repository = Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
|
||||
|
||||
// Build root view
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.notification_dialog_fragment, null)
|
||||
// topicNameText = view.findViewById(R.id.add_dialog_topic_text) as TextInputEditText
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_notification_dialog, null)
|
||||
|
||||
// Build dialog
|
||||
val alert = AlertDialog.Builder(activity)
|
||||
muteFor30minButton = view.findViewById(R.id.notification_dialog_30min)
|
||||
muteFor30minButton.setOnClickListener { onClickMinutes(30) }
|
||||
|
||||
muteFor1hButton = view.findViewById(R.id.notification_dialog_1h)
|
||||
muteFor1hButton.setOnClickListener { onClickMinutes(60) }
|
||||
|
||||
muteFor2hButton = view.findViewById(R.id.notification_dialog_2h)
|
||||
muteFor2hButton.setOnClickListener { onClickMinutes(2 * 60) }
|
||||
|
||||
muteFor8hButton = view.findViewById(R.id.notification_dialog_8h)
|
||||
muteFor8hButton.setOnClickListener{ onClickMinutes(8 * 60) }
|
||||
|
||||
muteUntilTomorrowButton = view.findViewById(R.id.notification_dialog_tomorrow)
|
||||
muteUntilTomorrowButton.setOnClickListener {
|
||||
val date = Calendar.getInstance()
|
||||
date.add(Calendar.DAY_OF_MONTH, 1)
|
||||
date.set(Calendar.HOUR_OF_DAY, 8)
|
||||
date.set(Calendar.MINUTE, 30)
|
||||
date.set(Calendar.SECOND, 0)
|
||||
date.set(Calendar.MILLISECOND, 0)
|
||||
onClick(date.timeInMillis/1000)
|
||||
}
|
||||
|
||||
muteForeverButton = view.findViewById(R.id.notification_dialog_forever)
|
||||
muteForeverButton.setOnClickListener{ onClick(1) }
|
||||
|
||||
return AlertDialog.Builder(activity)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.notification_dialog_save) { _, _ ->
|
||||
///
|
||||
settingsListener.onNotificationSettingsChanged(0L)
|
||||
}
|
||||
.setNegativeButton(R.string.notification_dialog_cancel) { _, _ ->
|
||||
dialog?.cancel()
|
||||
}
|
||||
.create()
|
||||
|
||||
// Add logic to disable "Subscribe" button on invalid input
|
||||
alert.setOnShowListener {
|
||||
val dialog = it as AlertDialog
|
||||
///
|
||||
}
|
||||
|
||||
return alert
|
||||
private fun onClickMinutes(minutes: Int) {
|
||||
onClick(System.currentTimeMillis()/1000 + minutes * 60)
|
||||
}
|
||||
|
||||
private fun onClick(mutedUntilTimestamp: Long) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
delay(150) // Another hack: Let the animation finish before dismissing the window
|
||||
settingsListener.onNotificationMutedUntilChanged(mutedUntilTimestamp)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NtfyNotificationFragment"
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import io.heckel.ntfy.R
|
|
@ -1,25 +0,0 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import io.heckel.ntfy.R
|
||||
|
||||
class SubscriptionSettingsActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_subscription_settings)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.subscription_settings_content, SubscriptionSettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
class SubscriptionSettingsFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ package io.heckel.ntfy.ui
|
|||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.Window
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
|
||||
fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
|
||||
|
@ -13,3 +15,8 @@ fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
|
|||
}
|
||||
statusBarColorAnimation.start()
|
||||
}
|
||||
|
||||
fun formatDateShort(timestampSecs: Long): String {
|
||||
val mutedUntilDate = Date(timestampSecs*1000)
|
||||
return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.withContext
|
|||
import kotlin.random.Random
|
||||
|
||||
class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
|
||||
// IMPORTANT WARNING:
|
||||
// Every time the worker is changed, the periodic work has to be REPLACEd.
|
||||
// This is facilitated in the MainActivity using the VERSION below.
|
||||
|
||||
|
@ -21,7 +22,8 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
|
|||
return withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Polling for new notifications")
|
||||
val database = Database.getInstance(applicationContext)
|
||||
val repository = Repository.getInstance(database.subscriptionDao(), database.notificationDao())
|
||||
val sharedPrefs = applicationContext.getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
val repository = Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
|
||||
val notifier = NotificationService(applicationContext)
|
||||
val api = ApiService()
|
||||
|
||||
|
@ -32,10 +34,8 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
|
|||
.onlyNewNotifications(subscription.id, notifications)
|
||||
.map { it.copy(notificationId = Random.nextInt()) }
|
||||
newNotifications.forEach { notification ->
|
||||
val added = repository.addNotification(notification)
|
||||
val detailViewOpen = repository.detailViewSubscriptionId.get() == subscription.id
|
||||
|
||||
if (added && !detailViewOpen) {
|
||||
val shouldNotify = repository.addNotification(notification)
|
||||
if (shouldNotify) {
|
||||
notifier.send(subscription, notification)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M11,21h-1l1,-7H7.5c-0.88,0 -0.33,-0.75 -0.31,-0.78C8.48,10.94 10.42,7.54 13.01,3h1l-1,7h3.51c0.4,0 0.62,0.19 0.4,0.66C12.97,17.55 11,21 11,21z"
|
||||
android:fillColor="#000000"/>
|
||||
android:fillColor="#555555"/>
|
||||
</vector>
|
|
@ -0,0 +1,12 @@
|
|||
<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,2.5C11.17,2.5 10.5,3.17 10.5,4L10.5,4.6797C9.556,4.9041 8.749,5.3367 8.0859,5.9219L9.5273,7.3672C10.1748,6.8243 11.0077,6.5 12,6.5C14.49,6.5 16,8.52 16,11L16,13.8555L18,15.8613L18,11C18,7.93 16.37,5.3597 13.5,4.6797L13.5,4C13.5,3.17 12.83,2.5 12,2.5zM6.7715,7.6289C6.2688,8.6106 6,9.762 6,11L6,16L4,18L4,19L18.1172,19L16,16.8789L16,17L8,17L8,11C8,10.3476 8.1073,9.7283 8.3066,9.168L6.7715,7.6289zM10,20C10,21.1 10.9,22 12,22C13.1,22 14,21.1 14,20L10,20z"
|
||||
android:fillColor="#555555"/>
|
||||
<path
|
||||
android:pathData="M3.543,3.3965 L2.0313,4.9043 19.5234,22.4395 21.0352,20.9316Z"
|
||||
android:fillColor="#555555"/>
|
||||
</vector>
|
|
@ -0,0 +1,15 @@
|
|||
<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,2.5C11.17,2.5 10.5,3.17 10.5,4L10.5,4.6797C9.556,4.9041 8.749,5.3367 8.0859,5.9219L9.5273,7.3672C10.1748,6.8243 11.0077,6.5 12,6.5C14.1766,6.5 15.6028,8.0429 15.9277,10.0879A6.6092,6.6092 0,0 1,16.502 10.0371A6.6092,6.6092 0,0 1,17.9609 10.2031C17.7024,7.4927 16.1179,5.2999 13.5,4.6797L13.5,4C13.5,3.17 12.83,2.5 12,2.5zM3.543,3.3965L2.0313,4.9043L6.2266,9.1094C6.0793,9.7072 6,10.3404 6,11L6,16L4,18L4,19L10.334,19A6.6092,6.6092 0,0 1,9.9238 17L8,17L8,11C8,10.9637 8.0032,10.9287 8.0039,10.8926L10.6738,13.5684A6.6092,6.6092 0,0 1,11.9824 11.8555L3.543,3.3965z"
|
||||
android:fillColor="#555555"/>
|
||||
<path
|
||||
android:pathData="m16.8553,10.7743c-3.3109,0 -6.002,2.6955 -6.002,6.0059 0,3.3104 2.6911,6.0078 6.002,6.0078 3.316,0 6.0117,-2.6969 6.0117,-6.0078 0,-3.3109 -2.6957,-6.0059 -6.0117,-6.0059zM16.8592,12.7861c2.2124,0 3.9941,1.7818 3.9941,3.9941 0,2.2124 -1.7818,3.9941 -3.9941,3.9941 -2.2124,0 -3.9941,-1.7818 -3.9941,-3.9941 0,-2.2124 1.7818,-3.9941 3.9941,-3.9941z"
|
||||
android:fillColor="#555555"/>
|
||||
<path
|
||||
android:pathData="m15.6308,13.426v4.041l3.5195,2.1113 0.8887,-1.4551 -2.6719,-1.5859v-3.1113h-0.4512z"
|
||||
android:fillColor="#555555"/>
|
||||
</vector>
|
|
@ -0,0 +1,15 @@
|
|||
<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,2.5C11.17,2.5 10.5,3.17 10.5,4L10.5,4.6797C9.556,4.9041 8.749,5.3367 8.0859,5.9219L9.5273,7.3672C10.1748,6.8243 11.0077,6.5 12,6.5C14.1766,6.5 15.6028,8.0429 15.9277,10.0879A6.6092,6.6092 0,0 1,16.502 10.0371A6.6092,6.6092 0,0 1,17.9609 10.2031C17.7024,7.4927 16.1179,5.2999 13.5,4.6797L13.5,4C13.5,3.17 12.83,2.5 12,2.5zM3.543,3.3965L2.0313,4.9043L6.2266,9.1094C6.0793,9.7072 6,10.3404 6,11L6,16L4,18L4,19L10.334,19A6.6092,6.6092 0,0 1,9.9238 17L8,17L8,11C8,10.9637 8.0032,10.9287 8.0039,10.8926L10.6738,13.5684A6.6092,6.6092 0,0 1,11.9824 11.8555L3.543,3.3965z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="m16.8553,10.7743c-3.3109,0 -6.002,2.6955 -6.002,6.0059 0,3.3104 2.6911,6.0078 6.002,6.0078 3.316,0 6.0117,-2.6969 6.0117,-6.0078 0,-3.3109 -2.6957,-6.0059 -6.0117,-6.0059zM16.8592,12.7861c2.2124,0 3.9941,1.7818 3.9941,3.9941 0,2.2124 -1.7818,3.9941 -3.9941,3.9941 -2.2124,0 -3.9941,-1.7818 -3.9941,-3.9941 0,-2.2124 1.7818,-3.9941 3.9941,-3.9941z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="m15.6308,13.426v4.041l3.5195,2.1113 0.8887,-1.4551 -2.6719,-1.5859v-3.1113h-0.4512z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
|
@ -0,0 +1,12 @@
|
|||
<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,2.5C11.17,2.5 10.5,3.17 10.5,4L10.5,4.6797C9.556,4.9041 8.749,5.3367 8.0859,5.9219L9.5273,7.3672C10.1748,6.8243 11.0077,6.5 12,6.5C14.49,6.5 16,8.52 16,11L16,13.8555L18,15.8613L18,11C18,7.93 16.37,5.3597 13.5,4.6797L13.5,4C13.5,3.17 12.83,2.5 12,2.5zM6.7715,7.6289C6.2688,8.6106 6,9.762 6,11L6,16L4,18L4,19L18.1172,19L16,16.8789L16,17L8,17L8,11C8,10.3476 8.1073,9.7283 8.3066,9.168L6.7715,7.6289zM10,20C10,21.1 10.9,22 12,22C13.1,22 14,21.1 14,20L10,20z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M3.543,3.3965 L2.0313,4.9043 19.5234,22.4395 21.0352,20.9316Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.SubscriptionSettingsActivity">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/subscription_settings_content" app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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/selectableItemBackground"
|
||||
android:orientation="vertical" android:clickable="true" android:focusable="true">
|
||||
<TextView
|
||||
android:text="Sun, October 31, 2021, 10:43:12"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_date_text"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"/>
|
||||
<TextView
|
||||
android:text="This is a very very very long message. It could be as long as 1024 charaters, which is a lot more than you'd think. No, really so far this message is barely 180 characters long. I can't believe how long 1024 bytes are. This is outrageous. Oh you know what, I think I won't type the whole thing. This seems a little too long for a sample text. Well, anyway, it was nice chatting. So far this message is about 400 bytes long. So maybe just double what you see and that's that."
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_message_text"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textColor="@color/primaryTextColor"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
android:layout_marginTop="-8dp" android:layout_marginBottom="-5dp" android:layout_marginStart="-3dp"/>
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_black_24dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_gray_24dp"
|
||||
android:id="@+id/add_dialog_instant_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_item_text"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_item_date" android:paddingTop="3dp"
|
39
app/src/main/res/layout/fragment_detail_item.xml
Normal file
39
app/src/main/res/layout/fragment_detail_item.xml
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:orientation="horizontal" android:clickable="true"
|
||||
android:focusable="true" android:paddingBottom="10dp"
|
||||
android:paddingTop="10dp" android:paddingStart="16dp"
|
||||
android:paddingEnd="10dp">
|
||||
<TextView
|
||||
android:text="Sun, October 31, 2021, 10:43:12"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_date_text"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
<TextView
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp" android:id="@+id/detail_item_new"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_circle"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toTopOf="@+id/detail_item_date_text"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/detail_item_date_text"
|
||||
android:layout_marginTop="1dp" app:layout_constraintStart_toEndOf="@+id/detail_item_date_text"
|
||||
android:layout_marginStart="5dp"/>
|
||||
<TextView
|
||||
android:text="This is a very very very long message. It could be as long as 1024 charaters, which is a lot more than you'd think. No, really so far this message is barely 180 characters long. I can't believe how long 1024 bytes are. This is outrageous. Oh you know what, I think I won't type the whole thing. This seems a little too long for a sample text. Well, anyway, it was nice chatting. So far this message is about 400 bytes long. So maybe just double what you see and that's that."
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_message_text"
|
||||
android:textColor="@color/primaryTextColor"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_item_date_text"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -32,9 +32,23 @@
|
|||
android:layout_marginBottom="10dp"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_black_24dp"
|
||||
android:id="@+id/main_item_instant_image"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_notifications_off_time_gray_outline_24dp"
|
||||
android:id="@+id/main_item_notification_disabled_until_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_item_text"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_item_notification_disabled_forever_image"
|
||||
android:paddingTop="3dp" android:layout_marginEnd="3dp"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_notifications_off_gray_outline_24dp"
|
||||
android:id="@+id/main_item_notification_disabled_forever_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_item_notification_disabled_until_image"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_item_instant_image" android:paddingTop="3dp"
|
||||
android:layout_marginEnd="3dp"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_gray_24dp"
|
||||
android:id="@+id/main_item_instant_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_item_notification_disabled_forever_image"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_item_date" android:paddingTop="3dp"/>
|
||||
<TextView
|
||||
android:text="10:13"
|
72
app/src/main/res/layout/fragment_notification_dialog.xml
Normal file
72
app/src/main/res/layout/fragment_notification_dialog.xml
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_dialog_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/notification_dialog_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" android:paddingStart="5dp" android:paddingEnd="5dp"
|
||||
android:textColor="@color/primaryTextColor"/>
|
||||
<RadioGroup
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/notification_dialog_title"
|
||||
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginBottom="10dp">
|
||||
<RadioButton
|
||||
android:text="@string/notification_dialog_30min"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/notification_dialog_30min"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:layout_marginTop="-3dp" android:layout_marginBottom="-3dp"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="@string/notification_dialog_1h"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/notification_dialog_1h"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:layout_marginTop="-3dp" android:layout_marginBottom="-3dp"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="@string/notification_dialog_2h"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/notification_dialog_2h"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:layout_marginTop="-3dp" android:layout_marginBottom="-3dp"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="@string/notification_dialog_8h"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/notification_dialog_8h"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:layout_marginTop="-3dp" android:layout_marginBottom="-3dp"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="@string/notification_dialog_tomorrow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/notification_dialog_tomorrow"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:layout_marginTop="-3dp" android:layout_marginBottom="-3dp"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="@string/notification_dialog_forever"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/notification_dialog_forever"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:layout_marginTop="-3dp" android:layout_marginBottom="-3dp"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,66 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_title_text2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:text="@string/notification_dialog_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="16dp" android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
<TextView
|
||||
android:text="Pause notifications"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:id="@+id/textView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/add_dialog_title_text2"
|
||||
android:layout_marginTop="20dp" app:layout_constraintStart_toStartOf="@+id/add_dialog_title_text2"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
||||
<RadioGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView" app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
android:layout_marginTop="10dp">
|
||||
<RadioButton
|
||||
android:text="Pause forever"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/radioButton"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="Pause for 30 minutes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/radioButton4"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="Pause for 1 hour"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/radioButton5"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="Pause for 2 hours"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/radioButton6"
|
||||
/>
|
||||
<RadioButton
|
||||
android:text="Pause until tomorrow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/radioButton3"
|
||||
/>
|
||||
</RadioGroup>
|
||||
<Switch
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:id="@+id/switch1"
|
||||
app:layout_constraintStart_toEndOf="@+id/textView" app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="19dp" app:layout_constraintTop_toBottomOf="@+id/add_dialog_title_text2"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,4 +0,0 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/main_menu_source" android:title="@string/main_menu_source_title"/>
|
||||
<item android:id="@+id/main_menu_website" android:title="@string/main_menu_website_title"/>
|
||||
</menu>
|
|
@ -1,6 +1,10 @@
|
|||
<menu xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/detail_menu_notification" android:title="@string/detail_menu_enable_instant"
|
||||
<item android:id="@+id/detail_menu_notifications_enabled" android:title="@string/detail_menu_notifications_enabled"
|
||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_white_24dp"/>
|
||||
<item android:id="@+id/detail_menu_notifications_disabled_until" android:title="@string/detail_menu_notifications_disabled_forever"
|
||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_time_white_outline_24dp"/>
|
||||
<item android:id="@+id/detail_menu_notifications_disabled_forever" android:title="@string/detail_menu_notifications_disabled_forever"
|
||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_white_outline_24dp"/>
|
||||
<item android:id="@+id/detail_menu_enable_instant" android:title="@string/detail_menu_enable_instant"
|
||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_bolt_outline_white_24dp"/>
|
||||
<item android:id="@+id/detail_menu_disable_instant" android:title="@string/detail_menu_disable_instant"
|
10
app/src/main/res/menu/menu_main_action_bar.xml
Normal file
10
app/src/main/res/menu/menu_main_action_bar.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/main_menu_notifications_enabled" android:title="@string/main_menu_notifications_enabled"
|
||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_white_24dp"/>
|
||||
<item android:id="@+id/main_menu_notifications_disabled_until" android:title="@string/main_menu_notifications_disabled_forever"
|
||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_time_white_outline_24dp"/>
|
||||
<item android:id="@+id/main_menu_notifications_disabled_forever" android:title="@string/detail_menu_notifications_disabled_forever"
|
||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_white_outline_24dp"/>
|
||||
<item android:id="@+id/main_menu_source" android:title="@string/main_menu_source_title"/>
|
||||
<item android:id="@+id/main_menu_website" android:title="@string/main_menu_website_title"/>
|
||||
</menu>
|
|
@ -1,16 +0,0 @@
|
|||
<resources>
|
||||
<!-- Reply Preference -->
|
||||
<string-array name="reply_entries">
|
||||
<item>Forever</item>
|
||||
<item>30 minutes</item>
|
||||
<item>1 hour</item>
|
||||
<item>2 hours</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="reply_values">
|
||||
<item>forever</item>
|
||||
<item>30min</item>
|
||||
<item>1hr</item>
|
||||
<item>2h</item>
|
||||
</string-array>
|
||||
</resources>
|
|
@ -10,8 +10,7 @@
|
|||
<string name="channel_subscriber_notification_text">You are subscribed to instant delivery topics</string>
|
||||
<string name="channel_subscriber_notification_text_one">You are subscribed to one instant delivery topic</string>
|
||||
<string name="channel_subscriber_notification_text_two">You are subscribed to two instant delivery topics</string>
|
||||
<string name="channel_subscriber_notification_text_three">You are subscribed to three instant delivery topics
|
||||
</string>
|
||||
<string name="channel_subscriber_notification_text_three">You are subscribed to three instant delivery topics</string>
|
||||
<string name="channel_subscriber_notification_text_four">You are subscribed to four instant delivery topics</string>
|
||||
<string name="channel_subscriber_notification_text_more">You are subscribed to %1$d instant delivery topics</string>
|
||||
|
||||
|
@ -22,13 +21,17 @@
|
|||
|
||||
<!-- Main activity: Action bar -->
|
||||
<string name="main_action_bar_title">Subscribed topics</string>
|
||||
<string name="main_menu_notifications_enabled">Notifications enabled</string>
|
||||
<string name="main_menu_notifications_disabled_forever">Notifications disabled</string>
|
||||
<string name="main_menu_notifications_disabled_until">Notifications disabled until %1$s</string>
|
||||
<string name="main_menu_source_title">Report a bug</string>
|
||||
<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_menu_unsubscribe">Unsubscribe</string>
|
||||
<string name="main_action_mode_delete_dialog_message">Do you really want to unsubscribe from selected topic(s) and
|
||||
<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>
|
||||
|
@ -41,7 +44,8 @@
|
|||
<string name="main_item_date_yesterday">Yesterday</string>
|
||||
<string name="main_add_button_description">Add subscription</string>
|
||||
<string name="main_no_subscriptions_text">It looks like you don\'t have any subscriptions yet.</string>
|
||||
<string name="main_how_to_intro">Click the button below to create or subscribe to a topic. After that, you can send
|
||||
<string name="main_how_to_intro">
|
||||
Click the button below to create or subscribe to a topic. After that, you can send
|
||||
messages via PUT or POST and you\'ll receive notifications on your phone.
|
||||
</string>
|
||||
<string name="main_how_to_link">For more detailed instructions, check out the ntfy.sh website and documentation.
|
||||
|
@ -49,7 +53,8 @@
|
|||
|
||||
<!-- Add dialog -->
|
||||
<string name="add_dialog_title">Subscribe to topic</string>
|
||||
<string name="add_dialog_description_below">Topics are not password-protected, so choose a name that\'s not easy to
|
||||
<string name="add_dialog_description_below">
|
||||
Topics are not password-protected, so choose a name that\'s not easy to
|
||||
guess. Once subscribed, you can PUT/POST to receive notifications on your phone.
|
||||
</string>
|
||||
<string name="add_dialog_topic_name_hint">Topic name, e.g. phils_alerts</string>
|
||||
|
@ -68,18 +73,15 @@
|
|||
|
||||
<!-- Detail activity -->
|
||||
<string name="detail_no_notifications_text">You haven\'t received any notifications for this topic yet.</string>
|
||||
<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_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_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 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>
|
||||
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It was sent at %1$s.</string>
|
||||
<string name="detail_test_message_error">Could not send test message: %1$s</string>
|
||||
<string name="detail_copied_to_clipboard_message">Copied to clipboard</string>
|
||||
<string name="detail_instant_delivery_enabled">Instant delivery enabled</string>
|
||||
|
@ -87,7 +89,9 @@
|
|||
<string name="detail_instant_info">Instant delivery cannot be disabled for subscriptions from other servers</string>
|
||||
|
||||
<!-- Detail activity: Action bar -->
|
||||
<string name="detail_menu_notification">Notification</string>
|
||||
<string name="detail_menu_notifications_enabled">Notifications enabled</string>
|
||||
<string name="detail_menu_notifications_disabled_forever">Notifications disabled</string>
|
||||
<string name="detail_menu_notifications_disabled_until">Notifications disabled until %1$s</string>
|
||||
<string name="detail_menu_enable_instant">Enable instant delivery</string>
|
||||
<string name="detail_menu_disable_instant">Disable instant delivery</string>
|
||||
<string name="detail_menu_test">Send test notification</string>
|
||||
|
@ -98,33 +102,22 @@
|
|||
<!-- Detail activity: Action mode -->
|
||||
<string name="detail_action_mode_menu_copy">Copy</string>
|
||||
<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 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>
|
||||
|
||||
<!-- Notification dialog -->
|
||||
<string name="notification_dialog_title">Notification settings</string>
|
||||
<string name="notification_dialog_title">Pause notifications</string>
|
||||
<string name="notification_dialog_cancel">Cancel</string>
|
||||
<string name="notification_dialog_save">Save</string>
|
||||
<string name="notification_dialog_enabled_toast_message">Notifications re-enabled</string>
|
||||
<string name="notification_dialog_muted_forever_toast_message">Notifications are now paused</string>
|
||||
<string name="notification_dialog_muted_until_toast_message">Notifications are now paused until %s</string>
|
||||
|
||||
|
||||
<!-- Preference Titles -->
|
||||
<string name="subscription_settings_notifications_header">Notifications</string>
|
||||
<string name="subscription_settings_pause_title">Pause notifications</string>
|
||||
<string name="subscription_settings_pause_for_title">Until …</string>
|
||||
<string name="sync_header">Sync</string>
|
||||
|
||||
<!-- Messages Preferences -->
|
||||
<string name="signature_title">Your signature</string>
|
||||
|
||||
<!-- Sync Preferences -->
|
||||
|
||||
<string name="attachment_title">Download incoming attachments</string>
|
||||
<string name="attachment_summary_on">Automatically download attachments for incoming emails</string>
|
||||
<string name="attachment_summary_off">Only download attachments when manually requested</string>
|
||||
<string name="notification_dialog_muted_until_toast_message">Notifications are now paused until %1$s</string>
|
||||
<string name="notification_dialog_30min">30 minutes</string>
|
||||
<string name="notification_dialog_1h">1 hour</string>
|
||||
<string name="notification_dialog_2h">2 hours</string>
|
||||
<string name="notification_dialog_8h">8 hours</string>
|
||||
<string name="notification_dialog_tomorrow">Until tomorrow</string>
|
||||
<string name="notification_dialog_forever">Forever</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="@string/subscription_settings_notifications_header">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="pause"
|
||||
app:title="@string/subscription_settings_pause_title"/>
|
||||
|
||||
<ListPreference
|
||||
app:key="reply"
|
||||
app:title="@string/subscription_settings_pause_for_title"
|
||||
app:entries="@array/reply_entries"
|
||||
app:entryValues="@array/reply_values"
|
||||
app:defaultValue="false"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:dependency="pause"
|
||||
/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
1
assets/notifications_black_24dp.svg
Normal file
1
assets/notifications_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>
|
After Width: | Height: | Size: 280 B |
1
assets/notifications_black_outline_24dp.svg
Normal file
1
assets/notifications_black_outline_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z"/></svg>
|
After Width: | Height: | Size: 366 B |
46
assets/notifications_off_black_outline_24dp.svg
Normal file
46
assets/notifications_off_black_outline_24dp.svg
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg870"
|
||||
sodipodi:docname="notifications_off_black_outline_24dp.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs874" />
|
||||
<sodipodi:namedview
|
||||
id="namedview872"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="19.563079"
|
||||
inkscape:cx="11.884632"
|
||||
inkscape:cy="18.606478"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg870" />
|
||||
<path
|
||||
d="M0 0h24v24H0V0z"
|
||||
fill="none"
|
||||
id="path866" />
|
||||
<path
|
||||
id="path868"
|
||||
d="M 12 2.5 C 11.17 2.5 10.5 3.17 10.5 4 L 10.5 4.6796875 C 9.5560271 4.9041286 8.7489952 5.336721 8.0859375 5.921875 L 9.5273438 7.3671875 C 10.17483 6.8242961 11.007683 6.5 12 6.5 C 14.49 6.5 16 8.52 16 11 L 16 13.855469 L 18 15.861328 L 18 11 C 18 7.93 16.37 5.3596875 13.5 4.6796875 L 13.5 4 C 13.5 3.17 12.83 2.5 12 2.5 z M 6.7714844 7.6289062 C 6.2688257 8.6105696 6 9.7619884 6 11 L 6 16 L 4 18 L 4 19 L 18.117188 19 L 16 16.878906 L 16 17 L 8 17 L 8 11 C 8 10.347635 8.1073172 9.7282789 8.3066406 9.1679688 L 6.7714844 7.6289062 z M 10 20 C 10 21.1 10.9 22 12 22 C 13.1 22 14 21.1 14 20 L 10 20 z " />
|
||||
<path
|
||||
style="color:#000000;fill:#000000;-inkscape-stroke:none"
|
||||
d="M 3.5429688,3.3964844 2.03125,4.9042969 19.523437,22.439453 21.035156,20.931641 Z"
|
||||
id="path989" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
60
assets/notifications_off_time_black_outline_24dp.svg
Normal file
60
assets/notifications_off_time_black_outline_24dp.svg
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg870"
|
||||
sodipodi:docname="notifications_off_time_black_outline_24dp.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs874" />
|
||||
<sodipodi:namedview
|
||||
id="namedview872"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="17.160655"
|
||||
inkscape:cx="17.977169"
|
||||
inkscape:cy="25.989683"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg870" />
|
||||
<path
|
||||
d="M0 0h24v24H0V0z"
|
||||
fill="none"
|
||||
id="path866" />
|
||||
<path
|
||||
id="path868"
|
||||
style="stroke:none;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
d="M 12 2.5 C 11.17 2.5 10.5 3.17 10.5 4 L 10.5 4.6796875 C 9.5560271 4.9041286 8.7489952 5.336721 8.0859375 5.921875 L 9.5273438 7.3671875 C 10.17483 6.8242961 11.007683 6.5 12 6.5 C 14.176573 6.5 15.602785 8.0428914 15.927734 10.087891 A 6.6092234 6.6092234 0 0 1 16.501953 10.037109 A 6.6092234 6.6092234 0 0 1 17.960938 10.203125 C 17.70244 7.4927147 16.117858 5.2999464 13.5 4.6796875 L 13.5 4 C 13.5 3.17 12.83 2.5 12 2.5 z M 3.5429688 3.3964844 L 2.03125 4.9042969 L 6.2265625 9.109375 C 6.0792507 9.7072066 6 10.340437 6 11 L 6 16 L 4 18 L 4 19 L 10.333984 19 A 6.6092234 6.6092234 0 0 1 9.9238281 17 L 8 17 L 8 11 C 8 10.96369 8.0032442 10.928679 8.0039062 10.892578 L 10.673828 13.568359 A 6.6092234 6.6092234 0 0 1 11.982422 11.855469 L 3.5429688 3.3964844 z " />
|
||||
<path
|
||||
id="path2323"
|
||||
style="color:#000000;fill:#000000;stroke-width:0.901647;-inkscape-stroke:none"
|
||||
d="m 16.85531,10.774346 c -3.310895,0 -6.001953,2.69551 -6.001953,6.005859 0,3.310352 2.691058,6.007813 6.001953,6.007813 3.316009,0 6.011719,-2.696918 6.011719,-6.007813 0,-3.310893 -2.69571,-6.005859 -6.011719,-6.005859 z m 0.0039,2.011719 c 2.212368,0 3.99414,1.781774 3.99414,3.99414 0,2.212369 -1.781772,3.994141 -3.99414,3.994141 -2.212368,0 -3.994141,-1.781772 -3.994141,-3.994141 0,-2.212367 1.781773,-3.99414 3.994141,-3.99414 z" />
|
||||
<g
|
||||
id="g2329"
|
||||
transform="translate(0.08197986,0.02172169)"
|
||||
style="fill:#000000">
|
||||
<path
|
||||
style="color:#000000;fill:#000000;stroke-width:0.901647;-inkscape-stroke:none"
|
||||
d="M 16.833417,13.855469 H 16 v 3.333667 l 2.916958,1.750175 0.416708,-0.683401 -2.50025,-1.483483 z"
|
||||
id="path2325" />
|
||||
<path
|
||||
style="color:#000000;fill:#000000;-inkscape-stroke:none"
|
||||
d="m 15.548828,13.404297 v 4.041015 l 3.519531,2.111329 0.888672,-1.455079 -2.671875,-1.585937 v -3.111328 h -0.451172 z"
|
||||
id="path2327" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
1
assets/schedule_black_24dp.svg
Normal file
1
assets/schedule_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
|
After Width: | Height: | Size: 352 B |
Loading…
Reference in a new issue