Priorities, titles and tags
|
@ -91,4 +91,7 @@ dependencies {
|
|||
// LiveData
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.liveDataVersion"
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
|
||||
// Emojis (tags and such)
|
||||
implementation 'com.vdurmont:emoji-java:5.1.1'
|
||||
}
|
||||
|
|
138
app/schemas/io.heckel.ntfy.data.Database/4.json
Normal file
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "06bd845a8d39dd10549f1aeb6b40d7c5",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Subscription",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "topic",
|
||||
"columnName": "topic",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instant",
|
||||
"columnName": "instant",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mutedUntil",
|
||||
"columnName": "mutedUntil",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Subscription_baseUrl_topic",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"baseUrl",
|
||||
"topic"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Notification",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`, `subscriptionId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subscriptionId",
|
||||
"columnName": "subscriptionId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "message",
|
||||
"columnName": "message",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationId",
|
||||
"columnName": "notificationId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "priority",
|
||||
"columnName": "priority",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deleted",
|
||||
"columnName": "deleted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id",
|
||||
"subscriptionId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '06bd845a8d39dd10549f1aeb6b40d7c5')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:name=".app.Application"
|
||||
|
|
|
@ -5,7 +5,6 @@ import androidx.room.*
|
|||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.*
|
||||
|
||||
@Entity(indices = [Index(value = ["baseUrl", "topic"], unique = true)])
|
||||
data class Subscription(
|
||||
|
@ -38,17 +37,20 @@ data class SubscriptionWithMetadata(
|
|||
val lastActive: Long
|
||||
)
|
||||
|
||||
@Entity
|
||||
@Entity(primaryKeys = ["id", "subscriptionId"])
|
||||
data class Notification(
|
||||
@PrimaryKey val id: String, // TODO make [id, subscriptionId] the primary key
|
||||
@ColumnInfo(name = "id") val id: String,
|
||||
@ColumnInfo(name = "subscriptionId") val subscriptionId: Long,
|
||||
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "message") val message: String,
|
||||
@ColumnInfo(name = "notificationId") val notificationId: Int, // Android notification popup ID
|
||||
@ColumnInfo(name = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max
|
||||
@ColumnInfo(name = "tags") val tags: String,
|
||||
@ColumnInfo(name = "deleted") val deleted: Boolean,
|
||||
)
|
||||
|
||||
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 3)
|
||||
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 4)
|
||||
abstract class Database : RoomDatabase() {
|
||||
abstract fun subscriptionDao(): SubscriptionDao
|
||||
abstract fun notificationDao(): NotificationDao
|
||||
|
@ -63,6 +65,7 @@ abstract class Database : RoomDatabase() {
|
|||
.databaseBuilder(context.applicationContext, Database::class.java,"AppDatabase")
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.addMigrations(MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_3_4)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
this.instance = instance
|
||||
|
@ -90,6 +93,15 @@ abstract class Database : RoomDatabase() {
|
|||
db.execSQL("ALTER TABLE Subscription ADD COLUMN mutedUntil INTEGER NOT NULL DEFAULT('0')")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE Notification_New (id TEXT NOT NULL, subscriptionId INTEGER NOT NULL, timestamp INTEGER NOT NULL, title TEXT NOT NULL, message TEXT NOT NULL, notificationId INTEGER NOT NULL, priority INTEGER NOT NULL, tags TEXT NOT NULL, deleted INTEGER NOT NULL, PRIMARY KEY(id, subscriptionId))")
|
||||
db.execSQL("INSERT INTO Notification_New SELECT id, subscriptionId, timestamp, '', message, notificationId, 3, '', deleted FROM Notification")
|
||||
db.execSQL("DROP TABLE Notification")
|
||||
db.execSQL("ALTER TABLE Notification_New RENAME TO Notification")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package io.heckel.ntfy.data
|
||||
|
||||
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
|
||||
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
|
||||
fun topicUrlJsonPoll(baseUrl: String, topic: String) = "${topicUrl(baseUrl, topic)}/json?poll=1"
|
||||
fun topicShortUrl(baseUrl: String, topic: String) =
|
||||
topicUrl(baseUrl, topic)
|
||||
.replace("http://", "")
|
||||
.replace("https://", "")
|
||||
|
|
@ -4,9 +4,11 @@ import android.util.Log
|
|||
import androidx.annotation.Keep
|
||||
import com.google.gson.Gson
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.topicUrl
|
||||
import io.heckel.ntfy.data.topicUrlJson
|
||||
import io.heckel.ntfy.data.topicUrlJsonPoll
|
||||
import io.heckel.ntfy.util.topicUrl
|
||||
import io.heckel.ntfy.util.topicUrlJson
|
||||
import io.heckel.ntfy.util.topicUrlJsonPoll
|
||||
import io.heckel.ntfy.util.toPriority
|
||||
import io.heckel.ntfy.util.joinTags
|
||||
import okhttp3.*
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.IOException
|
||||
|
@ -26,12 +28,21 @@ class ApiService {
|
|||
.readTimeout(77, TimeUnit.SECONDS) // Assuming that keepalive messages are more frequent than this
|
||||
.build()
|
||||
|
||||
fun publish(baseUrl: String, topic: String, message: String) {
|
||||
fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List<String>) {
|
||||
val url = topicUrl(baseUrl, topic)
|
||||
Log.d(TAG, "Publishing to $url")
|
||||
|
||||
val request = Request.Builder().url(url).put(message.toRequestBody()).build();
|
||||
client.newCall(request).execute().use { response ->
|
||||
var builder = Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("X-Priority", priority.toString())
|
||||
.put(message.toRequestBody())
|
||||
if (tags.isNotEmpty()) {
|
||||
builder = builder.addHeader("X-Tags", tags.joinToString(","))
|
||||
}
|
||||
if (title.isNotEmpty()) {
|
||||
builder = builder.addHeader("X-Title", title)
|
||||
}
|
||||
client.newCall(builder.build()).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Unexpected response ${response.code} when publishing to $url")
|
||||
}
|
||||
|
@ -87,7 +98,10 @@ class ApiService {
|
|||
id = message.id,
|
||||
subscriptionId = 0, // TO BE SET downstream
|
||||
timestamp = message.time,
|
||||
title = message.title ?: "",
|
||||
message = message.message,
|
||||
priority = toPriority(message.priority),
|
||||
tags = joinTags(message.tags),
|
||||
notificationId = Random.nextInt(),
|
||||
deleted = false
|
||||
)
|
||||
|
@ -109,7 +123,17 @@ class ApiService {
|
|||
|
||||
private fun fromString(subscriptionId: Long, s: String): Notification {
|
||||
val message = gson.fromJson(s, Message::class.java)
|
||||
return Notification(message.id, subscriptionId, message.time, message.message, notificationId = 0, deleted = false)
|
||||
return Notification(
|
||||
id = message.id,
|
||||
subscriptionId = subscriptionId,
|
||||
timestamp = message.time,
|
||||
title = message.title ?: "",
|
||||
message = message.message,
|
||||
priority = toPriority(message.priority),
|
||||
tags = joinTags(message.tags),
|
||||
notificationId = 0,
|
||||
deleted = false
|
||||
)
|
||||
}
|
||||
|
||||
/* This annotation ensures that proguard still works in production builds,
|
||||
|
@ -120,6 +144,9 @@ class ApiService {
|
|||
val time: Long,
|
||||
val event: String,
|
||||
val topic: String,
|
||||
val priority: Int?,
|
||||
val tags: List<String>?,
|
||||
val title: String?,
|
||||
val message: String
|
||||
)
|
||||
|
||||
|
|
|
@ -6,23 +6,25 @@ import android.app.PendingIntent
|
|||
import android.app.TaskStackBuilder
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.data.topicShortUrl
|
||||
import io.heckel.ntfy.util.topicShortUrl
|
||||
import io.heckel.ntfy.ui.DetailActivity
|
||||
import io.heckel.ntfy.ui.MainActivity
|
||||
import kotlin.random.Random
|
||||
import io.heckel.ntfy.util.formatMessage
|
||||
import io.heckel.ntfy.util.formatTitle
|
||||
|
||||
class NotificationService(val context: Context) {
|
||||
fun send(subscription: Subscription, notification: Notification) {
|
||||
val title = topicShortUrl(subscription.baseUrl, subscription.topic)
|
||||
Log.d(TAG, "Displaying notification $title: ${notification.message}")
|
||||
Log.d(TAG, "Displaying notification $notification")
|
||||
|
||||
// Create an Intent for the activity you want to start
|
||||
val intent = Intent(context, DetailActivity::class.java)
|
||||
|
@ -36,22 +38,33 @@ class NotificationService(val context: Context) {
|
|||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // Get the PendingIntent containing the entire back stack
|
||||
}
|
||||
|
||||
val title = formatTitle(subscription, notification)
|
||||
val message = formatMessage(notification)
|
||||
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
||||
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
val channelId = toChannelId(notification.priority)
|
||||
var notificationBuilder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setColor(ContextCompat.getColor(context, R.color.primaryColor))
|
||||
.setContentTitle(title)
|
||||
.setContentText(notification.message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(notification.message))
|
||||
.setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setSound(defaultSoundUri)
|
||||
.setContentIntent(pendingIntent) // Click target for notification
|
||||
.setAutoCancel(true) // Cancel when notification is clicked
|
||||
|
||||
if (notification.priority == 4) {
|
||||
notificationBuilder = notificationBuilder
|
||||
.setVibrate(longArrayOf(500, 500, 500, 500, 500, 500))
|
||||
.setLights(Color.YELLOW, 3000, 3000)
|
||||
} else if (notification.priority == 5) {
|
||||
notificationBuilder = notificationBuilder
|
||||
.setVibrate(longArrayOf(1000, 500, 1000, 500, 1000, 500))
|
||||
.setLights(Color.RED, 3000, 3000)
|
||||
}
|
||||
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelName = context.getString(R.string.channel_notifications_name) // Show's up in UI
|
||||
val channel = NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
createNotificationChannel(notificationManager, notification)
|
||||
}
|
||||
notificationManager.notify(notification.notificationId, notificationBuilder.build())
|
||||
}
|
||||
|
@ -64,8 +77,34 @@ class NotificationService(val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(notificationManager: NotificationManager, notification: Notification) {
|
||||
val channel = when (notification.priority) {
|
||||
1 -> NotificationChannel(CHANNEL_ID_MIN, context.getString(R.string.channel_notifications_min_name), NotificationManager.IMPORTANCE_MIN)
|
||||
2 -> NotificationChannel(CHANNEL_ID_LOW, context.getString(R.string.channel_notifications_low_name), NotificationManager.IMPORTANCE_LOW)
|
||||
4 -> NotificationChannel(CHANNEL_ID_HIGH, context.getString(R.string.channel_notifications_high_name), NotificationManager.IMPORTANCE_HIGH)
|
||||
5 -> NotificationChannel(CHANNEL_ID_MAX, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_MAX)
|
||||
else -> NotificationChannel(CHANNEL_ID_DEFAULT, context.getString(R.string.channel_notifications_default_name), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun toChannelId(priority: Int): String {
|
||||
return when (priority) {
|
||||
1 -> CHANNEL_ID_MIN
|
||||
2 -> CHANNEL_ID_LOW
|
||||
4 -> CHANNEL_ID_HIGH
|
||||
5 -> CHANNEL_ID_MAX
|
||||
else -> CHANNEL_ID_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NtfyNotificationService"
|
||||
private const val CHANNEL_ID = "ntfy"
|
||||
private const val CHANNEL_ID_MIN = "ntfy-min"
|
||||
private const val CHANNEL_ID_LOW = "ntfy-low"
|
||||
private const val CHANNEL_ID_DEFAULT = "ntfy"
|
||||
private const val CHANNEL_ID_HIGH = "ntfy-high"
|
||||
private const val CHANNEL_ID_MAX = "ntfy-max"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.util.Log
|
|||
import io.heckel.ntfy.data.ConnectionState
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.data.topicUrl
|
||||
import io.heckel.ntfy.util.topicUrl
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.Call
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
|
|
@ -15,7 +15,7 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.data.ConnectionState
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.data.topicUrl
|
||||
import io.heckel.ntfy.util.topicUrl
|
||||
import io.heckel.ntfy.ui.MainActivity
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
|
|
@ -121,8 +121,10 @@ class AddFragment : DialogFragment() {
|
|||
if (baseUrls.count() == 1) {
|
||||
baseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
|
||||
baseUrlText.setText(baseUrls.first())
|
||||
} else {
|
||||
} else if (baseUrls.count() > 1) {
|
||||
baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
|
||||
} else {
|
||||
baseUrlLayout.setEndIconDrawable(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,13 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.data.topicShortUrl
|
||||
import io.heckel.ntfy.data.topicUrl
|
||||
import io.heckel.ntfy.util.topicShortUrl
|
||||
import io.heckel.ntfy.util.topicUrl
|
||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import io.heckel.ntfy.msg.NotificationService
|
||||
import io.heckel.ntfy.util.fadeStatusBarColor
|
||||
import io.heckel.ntfy.util.formatDateShort
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
@ -324,8 +326,12 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val message = getString(R.string.detail_test_message, Date().toString())
|
||||
api.publish(subscriptionBaseUrl, subscriptionTopic, message)
|
||||
val possibleTags = listOf("warning", "skull", "success", "triangular_flag_on_post", "de", "dog", "rotating_light", "cat", "bike")
|
||||
val priority = Random.nextInt(1, 6)
|
||||
val tags = possibleTags.shuffled().take(Random.nextInt(0, 3))
|
||||
val title = if (Random.nextBoolean()) getString(R.string.detail_test_title) else ""
|
||||
val message = getString(R.string.detail_test_message, priority)
|
||||
api.publish(subscriptionBaseUrl, subscriptionTopic, message, title, priority, tags)
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
Toast
|
||||
|
|
|
@ -3,12 +3,16 @@ package io.heckel.ntfy.ui
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.util.formatMessage
|
||||
import io.heckel.ntfy.util.formatTitle
|
||||
import java.util.*
|
||||
|
||||
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
||||
|
@ -39,20 +43,51 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
|||
class DetailViewHolder(itemView: View, private val selected: Set<String>, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
private var notification: Notification? = null
|
||||
private val priorityImageView: ImageView = itemView.findViewById(R.id.detail_item_priority_image)
|
||||
private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text)
|
||||
private val titleView: TextView = itemView.findViewById(R.id.detail_item_title_text)
|
||||
private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text)
|
||||
private val newImageView: View = itemView.findViewById(R.id.detail_item_new)
|
||||
private val newImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
|
||||
|
||||
fun bind(notification: Notification) {
|
||||
this.notification = notification
|
||||
|
||||
dateView.text = Date(notification.timestamp * 1000).toString()
|
||||
messageView.text = notification.message
|
||||
messageView.text = formatMessage(notification)
|
||||
newImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE
|
||||
itemView.setOnClickListener { onClick(notification) }
|
||||
itemView.setOnLongClickListener { onLongClick(notification); true }
|
||||
if (notification.title != "") {
|
||||
titleView.visibility = View.VISIBLE
|
||||
titleView.text = formatTitle(notification)
|
||||
} else {
|
||||
titleView.visibility = View.GONE
|
||||
}
|
||||
if (selected.contains(notification.id)) {
|
||||
itemView.setBackgroundResource(R.color.primarySelectedRowColor);
|
||||
}
|
||||
val ctx = itemView.context
|
||||
when (notification.priority) {
|
||||
1 -> {
|
||||
priorityImageView.visibility = View.VISIBLE
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_1_24dp))
|
||||
}
|
||||
2 -> {
|
||||
priorityImageView.visibility = View.VISIBLE
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_2_24dp))
|
||||
}
|
||||
3 -> {
|
||||
priorityImageView.visibility = View.GONE
|
||||
}
|
||||
4 -> {
|
||||
priorityImageView.visibility = View.VISIBLE
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_4_24dp))
|
||||
}
|
||||
5 -> {
|
||||
priorityImageView.visibility = View.VISIBLE
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_5_24dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,11 +22,13 @@ import androidx.work.*
|
|||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.data.topicShortUrl
|
||||
import io.heckel.ntfy.util.topicShortUrl
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import io.heckel.ntfy.msg.NotificationService
|
||||
import io.heckel.ntfy.work.PollWorker
|
||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||
import io.heckel.ntfy.util.fadeStatusBarColor
|
||||
import io.heckel.ntfy.util.formatDateShort
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
|
|
@ -11,11 +11,10 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.data.ConnectionState
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.data.topicShortUrl
|
||||
import io.heckel.ntfy.util.topicShortUrl
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLongClick: (Subscription) -> Unit) :
|
||||
ListAdapter<Subscription, MainAdapter.SubscriptionViewHolder>(TopicDiffCallback) {
|
||||
val selected = mutableSetOf<Long>() // Subscription IDs
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
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) {
|
||||
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
|
||||
statusBarColorAnimation.addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
window.statusBarColor = color
|
||||
}
|
||||
statusBarColorAnimation.start()
|
||||
}
|
||||
|
||||
fun formatDateShort(timestampSecs: Long): String {
|
||||
val mutedUntilDate = Date(timestampSecs*1000)
|
||||
return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate)
|
||||
}
|
95
app/src/main/java/io/heckel/ntfy/util/Util.kt
Normal file
|
@ -0,0 +1,95 @@
|
|||
package io.heckel.ntfy.util
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.Window
|
||||
import com.vdurmont.emoji.EmojiManager
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
|
||||
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
|
||||
fun topicUrlJsonPoll(baseUrl: String, topic: String) = "${topicUrl(baseUrl, topic)}/json?poll=1"
|
||||
fun topicShortUrl(baseUrl: String, topic: String) =
|
||||
topicUrl(baseUrl, topic)
|
||||
.replace("http://", "")
|
||||
.replace("https://", "")
|
||||
|
||||
fun formatDateShort(timestampSecs: Long): String {
|
||||
val mutedUntilDate = Date(timestampSecs*1000)
|
||||
return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate)
|
||||
}
|
||||
|
||||
fun toPriority(priority: Int?): Int {
|
||||
if (priority != null && (1..5).contains(priority)) return priority
|
||||
else return 3
|
||||
}
|
||||
|
||||
fun joinTags(tags: List<String>?): String {
|
||||
return tags?.joinToString(",") ?: ""
|
||||
}
|
||||
|
||||
fun toTags(tags: String?): String {
|
||||
return tags ?: ""
|
||||
}
|
||||
|
||||
fun emojify(tags: List<String>): List<String> {
|
||||
return tags.mapNotNull {
|
||||
when (it.toLowerCase()) {
|
||||
"warn", "warning" -> "\u26A0\uFE0F"
|
||||
"success" -> "\u2714\uFE0F"
|
||||
"failure" -> "\u274C"
|
||||
else -> EmojiManager.getForAlias(it)?.unicode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend tags/emojis to message, but only if there is a non-empty title.
|
||||
* Otherwise the tags will be prepended to the title.
|
||||
*/
|
||||
fun formatMessage(notification: Notification): String {
|
||||
return if (notification.title != "") {
|
||||
notification.message
|
||||
} else {
|
||||
val emojis = emojify(notification.tags.split(","))
|
||||
if (emojis.isEmpty()) {
|
||||
notification.message
|
||||
} else {
|
||||
emojis.joinToString("") + " " + notification.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See above; prepend emojis to title if the title is non-empty.
|
||||
* Otherwise, they are prepended to the message.
|
||||
*/
|
||||
fun formatTitle(subscription: Subscription, notification: Notification): String {
|
||||
return if (notification.title != "") {
|
||||
formatTitle(notification)
|
||||
} else {
|
||||
topicShortUrl(subscription.baseUrl, subscription.topic)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatTitle(notification: Notification): String {
|
||||
val emojis = emojify(notification.tags.split(","))
|
||||
return if (emojis.isEmpty()) {
|
||||
notification.title
|
||||
} else {
|
||||
emojis.joinToString("") + " " + notification.title
|
||||
}
|
||||
}
|
||||
|
||||
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
|
||||
fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
|
||||
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
|
||||
statusBarColorAnimation.addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
window.statusBarColor = color
|
||||
}
|
||||
statusBarColorAnimation.start()
|
||||
}
|
26
app/src/main/res/drawable/ic_priority_1_24dp.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<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.195,20.8283a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277l-5.9848,3.6353 -5.9848,-3.6353a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#999999"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="m12.195,15.694a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372A1.2746,1.2746 0,0 0,19.9307 9.7205,1.2746 1.2746,0 0,0 18.1798,9.2928L12.195,12.9281 6.2102,9.2928a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#b3b3b3"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="m12.1168,10.4268a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277l-5.9848,3.6353 -5.9848,-3.6353a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511L11.455,10.2416a1.2747,1.2747 0,0 0,0.6618 0.1852z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#cccccc"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
19
app/src/main/res/drawable/ic_priority_2_24dp.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<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.1727,17.7744a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277L12.1727,15.0085 6.1879,11.3731a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#999999"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="m12.1727,12.64a1.2747,1.2747 0,0 0,0.6616 -0.1852L19.4809,8.4177A1.2746,1.2746 0,0 0,19.9084 6.6666,1.2746 1.2746,0 0,0 18.1575,6.2388L12.1727,9.8742 6.1879,6.2388a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#b3b3b3"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
20
app/src/main/res/drawable/ic_priority_4_24dp.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<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.1168,6.5394A1.2747,1.2747 0,0 0,11.4552 6.7246l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353A1.2746,1.2746 0,0 0,19.8525 12.5129,1.2746 1.2746,0 0,0 19.425,10.7618L12.7786,6.7246A1.2747,1.2747 0,0 0,12.1168 6.5394Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#c60000"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="m12.195,11.8067a1.2747,1.2747 0,0 0,-0.6616 0.1852l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353a1.2746,1.2746 0,0 0,1.7509 -0.4277,1.2746 1.2746,0 0,0 -0.4275,-1.7511l-6.6464,-4.0372a1.2747,1.2747 0,0 0,-0.6618 -0.1852z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#de0000"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
26
app/src/main/res/drawable/ic_priority_5_24dp.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<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.1168,3.4051A1.2747,1.2747 0,0 0,11.4552 3.5903L4.8086,7.6275A1.2746,1.2746 0,0 0,4.381 9.3786,1.2746 1.2746,0 0,0 6.132,9.8063L12.1168,6.171 18.1016,9.8063A1.2746,1.2746 0,0 0,19.8525 9.3786,1.2746 1.2746,0 0,0 19.425,7.6275L12.7786,3.5903A1.2747,1.2747 0,0 0,12.1168 3.4051Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#aa0000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12.1168,8.5394A1.2747,1.2747 0,0 0,11.4552 8.7246l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353A1.2746,1.2746 0,0 0,19.8525 14.5129,1.2746 1.2746,0 0,0 19.425,12.7618L12.7786,8.7246A1.2747,1.2747 0,0 0,12.1168 8.5394Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#c60000"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="m12.195,13.8067a1.2747,1.2747 0,0 0,-0.6616 0.1852l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353a1.2746,1.2746 0,0 0,1.7509 -0.4277,1.2746 1.2746,0 0,0 -0.4275,-1.7511l-6.6464,-4.0372a1.2747,1.2747 0,0 0,-0.6618 -0.1852z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.0919748"
|
||||
android:fillColor="#de0000"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -6,34 +6,58 @@
|
|||
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">
|
||||
android:focusable="true"
|
||||
>
|
||||
|
||||
<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"/>
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginTop="10dp" app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="10dp"/>
|
||||
<TextView
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp" android:id="@+id/detail_item_new"
|
||||
android:layout_height="10dp" android:id="@+id/detail_item_new_dot"
|
||||
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_marginTop="1dp"
|
||||
app:layout_constraintStart_toEndOf="@id/detail_item_priority_image"
|
||||
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_width="0dp"
|
||||
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"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/detail_item_title_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"/>
|
||||
<TextView
|
||||
android:text="This is an optional title. It can also be a little longer but not too long."
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_title_text"
|
||||
android:textColor="@color/primaryTextColor"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="10dp" android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_item_date_text"/>
|
||||
<ImageView
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp" app:srcCompat="@drawable/ic_priority_5_24dp"
|
||||
android:id="@+id/detail_item_priority_image"
|
||||
app:layout_constraintStart_toEndOf="@+id/detail_item_date_text"
|
||||
app:layout_constraintTop_toTopOf="@+id/detail_item_date_text"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/detail_item_date_text" android:layout_marginStart="5dp"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
@ -9,5 +9,8 @@
|
|||
|
||||
<color name="primarySelectedRowColor">#EEEEEE</color>
|
||||
<color name="primaryDangerButtonColor">#C30000</color>
|
||||
|
||||
<color name="primaryPriorityUrgentColor">#C30000</color>
|
||||
<color name="primaryPriorityHighColor">#E10000</color>
|
||||
</resources>
|
||||
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
<string name="app_base_host">ntfy.sh</string> <!-- If changed, you must also change google-services.json! -->
|
||||
|
||||
<!-- Notification channels -->
|
||||
<string name="channel_notifications_name">Notifications</string>
|
||||
<string name="channel_notifications_min_name">Notifications (Min Priority)</string>
|
||||
<string name="channel_notifications_low_name">Notifications (Low Priority)</string>
|
||||
<string name="channel_notifications_default_name">Notifications (Default Priority)</string>
|
||||
<string name="channel_notifications_high_name">Notifications (High Priority)</string>
|
||||
<string name="channel_notifications_max_name">Notifications (Max Priority)</string>
|
||||
<string name="channel_subscriber_service_name">Subscription Service</string>
|
||||
<string name="channel_subscriber_notification_title">Listening for incoming notifications</string>
|
||||
<string name="channel_subscriber_notification_text">You are subscribed to instant delivery topics</string>
|
||||
|
@ -88,7 +92,8 @@
|
|||
</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_title">Test: You can set a title if you like</string>
|
||||
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It has a priority of %1$d. If you send another one, it may look different.</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>
|
||||
|
|
|
@ -7,6 +7,9 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.msg.NotificationService
|
||||
import io.heckel.ntfy.util.joinTags
|
||||
import io.heckel.ntfy.util.toPriority
|
||||
import io.heckel.ntfy.util.toTags
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -29,7 +32,10 @@ class FirebaseService : FirebaseMessagingService() {
|
|||
val id = data["id"]
|
||||
val timestamp = data["time"]?.toLongOrNull()
|
||||
val topic = data["topic"]
|
||||
val title = data["title"]
|
||||
val message = data["message"]
|
||||
val priority = data["priority"]?.toIntOrNull()
|
||||
val tags = data["tags"]
|
||||
if (id == null || topic == null || message == null || timestamp == null) {
|
||||
Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, data=${data}")
|
||||
return
|
||||
|
@ -45,8 +51,11 @@ class FirebaseService : FirebaseMessagingService() {
|
|||
id = id,
|
||||
subscriptionId = subscription.id,
|
||||
timestamp = timestamp,
|
||||
title = title ?: "",
|
||||
message = message,
|
||||
notificationId = Random.nextInt(),
|
||||
priority = toPriority(priority),
|
||||
tags = toTags(tags),
|
||||
deleted = false
|
||||
)
|
||||
val shouldNotify = repository.addNotification(notification)
|
||||
|
|
1
assets/arrow_drop_down_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="M7 10l5 5 5-5z"/></svg>
|
After Width: | Height: | Size: 171 B |
47
assets/priority_1_24dp.svg
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?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="svg1428"
|
||||
sodipodi:docname="priority_1_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="defs1432" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1430"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="20.517358"
|
||||
inkscape:cx="22.834324"
|
||||
inkscape:cy="15.742768"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1428" />
|
||||
<path
|
||||
style="color:#000000;fill:#999999;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.195014,20.828316 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037178 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751107 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 l -5.984807,3.635327 -5.9848086,-3.635327 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427718 1.2745823,1.2745823 0 0 0 0.427536,1.751107 l 6.6464146,4.037178 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
|
||||
id="rect3554" />
|
||||
<path
|
||||
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.195014,15.694014 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037176 A 1.2745823,1.2745823 0 0 0 19.930749,9.7205243 1.2745823,1.2745823 0 0 0 18.179821,9.2928073 L 12.195014,12.928134 6.2102054,9.2928073 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427717 1.2745823,1.2745823 0 0 0 0.427536,1.7511077 l 6.6464146,4.037176 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
|
||||
id="path9314" />
|
||||
<path
|
||||
style="color:#000000;fill:#cccccc;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.116784,10.426777 a 1.2747098,1.2747098 0 0 0 0.661606,-0.185205 l 6.646593,-4.0371767 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751108 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 l -5.984808,3.635327 -5.9848066,-3.635327 a 1.2745823,1.2745823 0 0 0 -1.750928,0.427718 1.2745823,1.2745823 0 0 0 0.427537,1.751108 L 11.455,10.241572 a 1.2747098,1.2747098 0 0 0 0.661784,0.185205 z"
|
||||
id="path9316" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
43
assets/priority_2_24dp.svg
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?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="svg1428"
|
||||
sodipodi:docname="priority_2_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="defs1432" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1430"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="20.517358"
|
||||
inkscape:cx="22.834324"
|
||||
inkscape:cy="15.742768"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1428" />
|
||||
<path
|
||||
style="color:#000000;fill:#999999;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.172712,17.774352 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037178 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751107 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 L 12.172712,15.00847 6.1879033,11.373143 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427718 1.2745823,1.2745823 0 0 0 0.427536,1.751107 l 6.6464147,4.037178 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
|
||||
id="rect3554" />
|
||||
<path
|
||||
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.172712,12.64005 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 L 19.48091,8.4176679 A 1.2745823,1.2745823 0 0 0 19.908447,6.6665602 1.2745823,1.2745823 0 0 0 18.157519,6.2388432 L 12.172712,9.8741699 6.1879033,6.2388432 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427717 1.2745823,1.2745823 0 0 0 0.427536,1.7511077 l 6.6464147,4.0371761 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
|
||||
id="path9314" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
43
assets/priority_4_24dp.svg
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?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="svg1428"
|
||||
sodipodi:docname="priority_4_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="defs1432" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1430"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="20.517358"
|
||||
inkscape:cx="22.834324"
|
||||
inkscape:cy="15.742768"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1428" />
|
||||
<path
|
||||
style="color:#000000;fill:#c60000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="M 12.116784,6.5394415 A 1.2747098,1.2747098 0 0 0 11.455179,6.724648 l -6.6465926,4.037176 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.7509281,0.427717 l 5.9848065,-3.635327 5.984809,3.635327 A 1.2745823,1.2745823 0 0 0 19.85252,12.512932 1.2745823,1.2745823 0 0 0 19.424984,10.761824 L 12.778569,6.724648 A 1.2747098,1.2747098 0 0 0 12.116784,6.5394415 Z"
|
||||
id="path9314" />
|
||||
<path
|
||||
style="color:#000000;fill:#de0000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.195014,11.806679 a 1.2747098,1.2747098 0 0 0 -0.661606,0.185205 l -6.6465924,4.037177 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.750928,0.427718 l 5.9848074,-3.635327 5.984807,3.635327 a 1.2745823,1.2745823 0 0 0 1.750928,-0.427718 1.2745823,1.2745823 0 0 0 -0.427537,-1.751108 l -6.646414,-4.037177 a 1.2747098,1.2747098 0 0 0 -0.661784,-0.185205 z"
|
||||
id="path9316" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
39
assets/priority_4_alt_24dp.svg
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?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="svg1428"
|
||||
sodipodi:docname="priority_4_alt_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="defs1432" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1430"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="19.300483"
|
||||
inkscape:cx="8.1604177"
|
||||
inkscape:cy="7.4609534"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1428" />
|
||||
<path
|
||||
style="color:#000000;fill:#aa0000;fill-opacity:1;stroke-width:0.0878234;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="M 12.073041,8.4104485 A 1.2171741,1.2171741 0 0 0 11.441298,8.5872953 L 5.0947071,12.442249 a 1.2170524,1.2170524 0 0 0 -0.4082396,1.672069 1.2170524,1.2170524 0 0 0 1.6718977,0.408412 l 5.7146758,-3.471242 5.714676,3.471242 a 1.2170524,1.2170524 0 0 0 1.671897,-0.408412 1.2170524,1.2170524 0 0 0 -0.408239,-1.672069 L 12.704955,8.5872953 A 1.2171741,1.2171741 0 0 0 12.073041,8.4104485 Z"
|
||||
id="rect3554" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
39
assets/priority_5_24dp.svg
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?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="svg1428"
|
||||
sodipodi:docname="priority_5_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="defs1432" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1430"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="26.806921"
|
||||
inkscape:cx="18.894374"
|
||||
inkscape:cy="14.026229"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1428" />
|
||||
<path
|
||||
d="M 16.748063,0.46162067 H 7.1927317 L 0.44251841,7.211834 v 9.555331 l 6.75021329,6.750213 h 9.5553313 l 6.750213,-6.750213 V 7.211834 Z M 11.970397,18.778139 c -0.922231,0 -1.665138,-0.742908 -1.665138,-1.665138 0,-0.92223 0.742907,-1.665138 1.665138,-1.665138 0.92223,0 1.665138,0.742908 1.665138,1.665138 0,0.92223 -0.742908,1.665138 -1.665138,1.665138 z m 1.280875,-5.507765 H 10.689521 V 5.5851222 h 2.561751 z"
|
||||
id="path29312"
|
||||
style="fill:#ac0000;fill-opacity:1;stroke-width:1.28088" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
47
assets/priority_5_alt2_24dp.svg
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?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="svg1428"
|
||||
sodipodi:docname="priority_5_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="defs1432" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1430"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="20.517358"
|
||||
inkscape:cx="22.834323"
|
||||
inkscape:cy="15.742767"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1428" />
|
||||
<path
|
||||
style="color:#000000;fill:#aa0000;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="M 12.116784,3.40514 A 1.2747098,1.2747098 0 0 0 11.455179,3.5903463 L 4.8085864,7.6275238 A 1.2745823,1.2745823 0 0 0 4.3810494,9.3786313 1.2745823,1.2745823 0 0 0 6.1319775,9.8063489 L 12.116784,6.1710217 18.101593,9.8063489 A 1.2745823,1.2745823 0 0 0 19.85252,9.3786313 1.2745823,1.2745823 0 0 0 19.424984,7.6275238 L 12.778569,3.5903463 A 1.2747098,1.2747098 0 0 0 12.116784,3.40514 Z"
|
||||
id="rect3554" />
|
||||
<path
|
||||
style="color:#000000;fill:#c60000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="M 12.116784,8.5394415 A 1.2747098,1.2747098 0 0 0 11.455179,8.724648 l -6.6465926,4.037176 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.7509281,0.427717 l 5.9848065,-3.635327 5.984809,3.635327 A 1.2745823,1.2745823 0 0 0 19.85252,14.512932 1.2745823,1.2745823 0 0 0 19.424984,12.761824 L 12.778569,8.724648 A 1.2747098,1.2747098 0 0 0 12.116784,8.5394415 Z"
|
||||
id="path9314" />
|
||||
<path
|
||||
style="color:#000000;fill:#de0000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.195014,13.806679 a 1.2747098,1.2747098 0 0 0 -0.661606,0.185205 l -6.6465924,4.037177 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.750928,0.427718 l 5.9848074,-3.635327 5.984807,3.635327 a 1.2745823,1.2745823 0 0 0 1.750928,-0.427718 1.2745823,1.2745823 0 0 0 -0.427537,-1.751108 l -6.646414,-4.037177 a 1.2747098,1.2747098 0 0 0 -0.661784,-0.185205 z"
|
||||
id="path9316" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
47
assets/priority_5_alt_24dp.svg
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?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="svg1428"
|
||||
sodipodi:docname="priority_5_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="defs1432" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1430"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="19.300483"
|
||||
inkscape:cx="8.1604177"
|
||||
inkscape:cy="7.4609534"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1428" />
|
||||
<path
|
||||
style="color:#000000;fill:#ff5630;fill-opacity:1;stroke-width:0.0878234;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="M 12.368788,3.4216973 A 1.2171741,1.2171741 0 0 0 11.737045,3.5985441 L 5.3904546,7.4534983 a 1.2170524,1.2170524 0 0 0 -0.4082396,1.672069 1.2170524,1.2170524 0 0 0 1.6718977,0.408412 l 5.7146753,-3.471242 5.714676,3.471242 a 1.2170524,1.2170524 0 0 0 1.671897,-0.408412 1.2170524,1.2170524 0 0 0 -0.408239,-1.672069 L 13.000702,3.5985441 A 1.2171741,1.2171741 0 0 0 12.368788,3.4216973 Z"
|
||||
id="rect3554" />
|
||||
<path
|
||||
style="color:#000000;fill:#ff7452;fill-opacity:1;stroke:none;stroke-width:0.0878234;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.368788,8.6910833 a 1.2171741,1.2171741 0 0 0 -0.631743,0.176847 L 5.3904546,12.722883 a 1.2170524,1.2170524 0 0 0 -0.4082396,1.67207 1.2170524,1.2170524 0 0 0 1.6718977,0.408411 l 5.7146753,-3.471242 5.714676,3.471242 a 1.2170524,1.2170524 0 0 0 1.671897,-0.408411 1.2170524,1.2170524 0 0 0 -0.408239,-1.67207 l -6.34642,-3.8549527 a 1.2171741,1.2171741 0 0 0 -0.631914,-0.176847 z"
|
||||
id="path9314" />
|
||||
<path
|
||||
style="color:#000000;fill:#ff8f73;fill-opacity:1;stroke:none;stroke-width:0.0878234;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 12.368788,14.136115 a 1.2171741,1.2171741 0 0 0 -0.631743,0.176846 l -6.3465904,3.854954 a 1.2170524,1.2170524 0 0 0 -0.4082396,1.672069 1.2170524,1.2170524 0 0 0 1.6718977,0.408412 l 5.7146753,-3.471242 5.714676,3.471242 a 1.2170524,1.2170524 0 0 0 1.671897,-0.408412 1.2170524,1.2170524 0 0 0 -0.408239,-1.672069 l -6.34642,-3.854954 a 1.2171741,1.2171741 0 0 0 -0.631914,-0.176846 z"
|
||||
id="path9316" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
1
assets/priority_high_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 0h24v24H0V0z" fill="none"/><circle cx="12" cy="19" r="2"/><path d="M10 3h4v12h-4z"/></svg>
|
After Width: | Height: | Size: 204 B |
44
assets/priority_high_circle_red_24dp.svg
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?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="svg874"
|
||||
sodipodi:docname="priority_high_circle_red_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="defs878" />
|
||||
<sodipodi:namedview
|
||||
id="namedview876"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="24.506368"
|
||||
inkscape:cx="13.894348"
|
||||
inkscape:cy="9.8749843"
|
||||
inkscape:window-width="1863"
|
||||
inkscape:window-height="1025"
|
||||
inkscape:window-x="57"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg874" />
|
||||
<path
|
||||
id="path25891"
|
||||
style="fill:#800000;stroke-width:34.5068;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="M 11.838253,0.22539283 A 11.693702,11.693702 0 0 0 0.14541272,11.918233 11.693702,11.693702 0 0 0 11.838253,23.612994 11.693702,11.693702 0 0 0 23.533014,11.918233 11.693702,11.693702 0 0 0 11.838253,0.22539283 Z M 11.918883,1.530783 A 10.412385,10.412385 0 0 1 22.331287,11.945109 10.412385,10.412385 0 0 1 11.918881,22.357515 10.412385,10.412385 0 0 1 1.5045542,11.945109 10.412385,10.412385 0 0 1 11.918881,1.530783 Z M 10.095174,3.8612884 V 14.592364 h 3.576385 V 3.8612884 Z m 1.789152,12.5202276 a 1.7886362,1.7886362 0 0 0 -1.789152,1.789152 1.7886362,1.7886362 0 0 0 1.789152,1.787233 1.7886362,1.7886362 0 0 0 1.787233,-1.787233 1.7886362,1.7886362 0 0 0 -1.787233,-1.789152 z" />
|
||||
<path
|
||||
d="M 0.14541272,0.22539283 H 23.734581 V 23.814561 H 0.14541272 Z"
|
||||
fill="none"
|
||||
id="path868"
|
||||
style="stroke-width:0.982882" />
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
1
assets/report_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="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM12 17.3c-.72 0-1.3-.58-1.3-1.3 0-.72.58-1.3 1.3-1.3.72 0 1.3.58 1.3 1.3 0 .72-.58 1.3-1.3 1.3zm1-4.3h-2V7h2v6z"/></svg>
|
After Width: | Height: | Size: 329 B |
1
assets/warning_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="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>
|
After Width: | Height: | Size: 207 B |