- Auto download toggle
- Do not update notification if not visible - Detail view menu
This commit is contained in:
parent
95e101eb65
commit
e88f87390e
19 changed files with 273 additions and 68 deletions
|
@ -68,9 +68,13 @@ data class Attachment(
|
||||||
@ColumnInfo(name = "progress") val progress: Int,
|
@ColumnInfo(name = "progress") val progress: Int,
|
||||||
) {
|
) {
|
||||||
constructor(name: String?, type: String?, size: Long?, expires: Long?, url: String) :
|
constructor(name: String?, type: String?, size: Long?, expires: Long?, url: String) :
|
||||||
this(name, type, size, expires, url, null, 0)
|
this(name, type, size, expires, url, null, PROGRESS_NONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val PROGRESS_NONE = -1
|
||||||
|
const val PROGRESS_INDETERMINATE = -2
|
||||||
|
const val PROGRESS_DONE = 100
|
||||||
|
|
||||||
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6)
|
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6)
|
||||||
abstract class Database : RoomDatabase() {
|
abstract class Database : RoomDatabase() {
|
||||||
abstract fun subscriptionDao(): SubscriptionDao
|
abstract fun subscriptionDao(): SubscriptionDao
|
||||||
|
|
|
@ -161,6 +161,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
|
||||||
return sharedPrefs.getInt(SHARED_PREFS_MIN_PRIORITY, 1) // 1/low means all priorities
|
return sharedPrefs.getInt(SHARED_PREFS_MIN_PRIORITY, 1) // 1/low means all priorities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAutoDownloadEnabled(): Boolean {
|
||||||
|
return sharedPrefs.getBoolean(SHARED_PREFS_AUTO_DOWNLOAD_ENABLED, true) // Enabled by default
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutoDownloadEnabled(enabled: Boolean) {
|
||||||
|
sharedPrefs.edit()
|
||||||
|
.putBoolean(SHARED_PREFS_AUTO_DOWNLOAD_ENABLED, enabled)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
fun getBroadcastEnabled(): Boolean {
|
fun getBroadcastEnabled(): Boolean {
|
||||||
return sharedPrefs.getBoolean(SHARED_PREFS_BROADCAST_ENABLED, true) // Enabled by default
|
return sharedPrefs.getBoolean(SHARED_PREFS_BROADCAST_ENABLED, true) // Enabled by default
|
||||||
}
|
}
|
||||||
|
@ -291,6 +301,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
|
||||||
const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
|
const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
|
||||||
const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil"
|
const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil"
|
||||||
const val SHARED_PREFS_MIN_PRIORITY = "MinPriority"
|
const val SHARED_PREFS_MIN_PRIORITY = "MinPriority"
|
||||||
|
const val SHARED_PREFS_AUTO_DOWNLOAD_ENABLED = "AutoDownload"
|
||||||
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
|
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
|
||||||
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
|
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
|
||||||
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
|
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
|
||||||
|
|
|
@ -8,11 +8,7 @@ import android.util.Log
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.data.Attachment
|
import io.heckel.ntfy.data.*
|
||||||
import io.heckel.ntfy.data.Notification
|
|
||||||
import io.heckel.ntfy.data.Repository
|
|
||||||
import io.heckel.ntfy.data.Subscription
|
|
||||||
import io.heckel.ntfy.msg.NotificationService.Companion.PROGRESS_DONE
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -33,12 +29,12 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
|
||||||
val repository = app.repository
|
val repository = app.repository
|
||||||
val notification = repository.getNotification(notificationId) ?: return Result.failure()
|
val notification = repository.getNotification(notificationId) ?: return Result.failure()
|
||||||
val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
||||||
val attachment = notification.attachment ?: return Result.failure()
|
downloadAttachment(repository, subscription, notification)
|
||||||
downloadAttachment(repository, subscription, notification, attachment)
|
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification, attachment: Attachment) {
|
private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification) {
|
||||||
|
val attachment = notification.attachment ?: return
|
||||||
Log.d(TAG, "Downloading attachment from ${attachment.url}")
|
Log.d(TAG, "Downloading attachment from ${attachment.url}")
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
|
@ -71,8 +67,11 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
|
||||||
var lastProgress = 0L
|
var lastProgress = 0L
|
||||||
while (bytes >= 0) {
|
while (bytes >= 0) {
|
||||||
if (System.currentTimeMillis() - lastProgress > 500) {
|
if (System.currentTimeMillis() - lastProgress > 500) {
|
||||||
val progress = if (size > 0) (bytesCopied.toFloat()/size.toFloat()*100).toInt() else NotificationService.PROGRESS_INDETERMINATE
|
val progress = if (size > 0) (bytesCopied.toFloat()/size.toFloat()*100).toInt() else PROGRESS_INDETERMINATE
|
||||||
notifier.update(subscription, notification, progress = progress)
|
val newAttachment = attachment.copy(progress = progress)
|
||||||
|
val newNotification = notification.copy(attachment = newAttachment)
|
||||||
|
notifier.update(subscription, newNotification)
|
||||||
|
repository.updateNotification(newNotification)
|
||||||
lastProgress = System.currentTimeMillis()
|
lastProgress = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
fileOut.write(buffer, 0, bytes)
|
fileOut.write(buffer, 0, bytes)
|
||||||
|
@ -81,10 +80,10 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Attachment download: successful response, proceeding with download")
|
Log.d(TAG, "Attachment download: successful response, proceeding with download")
|
||||||
val newAttachment = attachment.copy(contentUri = uri.toString())
|
val newAttachment = attachment.copy(contentUri = uri.toString(), progress = PROGRESS_DONE)
|
||||||
val newNotification = notification.copy(attachment = newAttachment)
|
val newNotification = notification.copy(attachment = newAttachment)
|
||||||
repository.updateNotification(newNotification)
|
repository.updateNotification(newNotification)
|
||||||
notifier.update(subscription, newNotification, progress = PROGRESS_DONE)
|
notifier.update(subscription, newNotification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldDownload(subscription: Subscription, notification: Notification): Boolean {
|
private fun shouldDownload(subscription: Subscription, notification: Notification): Boolean {
|
||||||
return notification.attachment != null
|
return notification.attachment != null && repository.getAutoDownloadEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
|
private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
|
||||||
|
@ -84,12 +84,10 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleAttachmentDownload(notification: Notification) {
|
private fun scheduleAttachmentDownload(notification: Notification) {
|
||||||
Log.d(TAG, "Enqueuing work to download attachment (+ preview if available)")
|
Log.d(TAG, "Enqueuing work to download attachment")
|
||||||
val workManager = WorkManager.getInstance(context)
|
val workManager = WorkManager.getInstance(context)
|
||||||
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
|
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
|
||||||
.setInputData(workDataOf(
|
.setInputData(workDataOf("id" to notification.id))
|
||||||
"id" to notification.id,
|
|
||||||
))
|
|
||||||
.build()
|
.build()
|
||||||
workManager.enqueue(workRequest)
|
workManager.enqueue(workRequest)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,15 +24,21 @@ class NotificationService(val context: Context) {
|
||||||
displayInternal(subscription, notification)
|
displayInternal(subscription, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(subscription: Subscription, notification: Notification, progress: Int = PROGRESS_NONE) {
|
fun update(subscription: Subscription, notification: Notification) {
|
||||||
Log.d(TAG, "Updating notification $notification")
|
val active = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
displayInternal(subscription, notification, update = true, progress = progress)
|
notificationManager.activeNotifications.find { it.id == notification.notificationId } != null
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
Log.d(TAG, "Updating notification $notification")
|
||||||
|
displayInternal(subscription, notification, update = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel(notification: Notification) {
|
fun cancel(notification: Notification) {
|
||||||
if (notification.notificationId != 0) {
|
if (notification.notificationId != 0) {
|
||||||
Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}")
|
Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}")
|
||||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
notificationManager.cancel(notification.notificationId)
|
notificationManager.cancel(notification.notificationId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,9 +47,9 @@ class NotificationService(val context: Context) {
|
||||||
(1..5).forEach { priority -> maybeCreateNotificationChannel(priority) }
|
(1..5).forEach { priority -> maybeCreateNotificationChannel(priority) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false, progress: Int = PROGRESS_NONE) {
|
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) {
|
||||||
val title = formatTitle(subscription, notification)
|
val title = formatTitle(subscription, notification)
|
||||||
val message = maybeWithAttachmentInfo(formatMessage(notification), notification, progress)
|
val message = maybeWithAttachmentInfo(formatMessage(notification), notification)
|
||||||
val channelId = toChannelId(notification.priority)
|
val channelId = toChannelId(notification.priority)
|
||||||
val builder = NotificationCompat.Builder(context, channelId)
|
val builder = NotificationCompat.Builder(context, channelId)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
@ -55,7 +61,7 @@ class NotificationService(val context: Context) {
|
||||||
setStyle(builder, notification, message) // Preview picture or big text style
|
setStyle(builder, notification, message) // Preview picture or big text style
|
||||||
setContentIntent(builder, subscription, notification)
|
setContentIntent(builder, subscription, notification)
|
||||||
maybeSetSound(builder, update)
|
maybeSetSound(builder, update)
|
||||||
maybeSetProgress(builder, progress)
|
maybeSetProgress(builder, notification)
|
||||||
maybeAddOpenAction(builder, notification)
|
maybeAddOpenAction(builder, notification)
|
||||||
maybeAddBrowseAction(builder, notification)
|
maybeAddBrowseAction(builder, notification)
|
||||||
|
|
||||||
|
@ -63,16 +69,17 @@ class NotificationService(val context: Context) {
|
||||||
notificationManager.notify(notification.notificationId, builder.build())
|
notificationManager.notify(notification.notificationId, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeWithAttachmentInfo(message: String, notification: Notification, progress: Int): String {
|
// FIXME duplicate code
|
||||||
if (progress < 0 || notification.attachment == null) return message
|
private fun maybeWithAttachmentInfo(message: String, notification: Notification): String {
|
||||||
val att = notification.attachment
|
val att = notification.attachment ?: return message
|
||||||
|
if (att.progress < 0) return message
|
||||||
val infos = mutableListOf<String>()
|
val infos = mutableListOf<String>()
|
||||||
if (att.name != null) infos.add(att.name)
|
if (att.name != null) infos.add(att.name)
|
||||||
if (att.size != null) infos.add(formatBytes(att.size))
|
if (att.size != null) infos.add(formatBytes(att.size))
|
||||||
//if (att.expires != null && att.expires != 0L) infos.add(formatDateShort(att.expires))
|
//if (att.expires != null && att.expires != 0L) infos.add(formatDateShort(att.expires))
|
||||||
if (progress in 0..99) infos.add("${progress}%")
|
if (att.progress in 0..99) infos.add("${att.progress}%")
|
||||||
if (infos.size == 0) return message
|
if (infos.size == 0) return message
|
||||||
if (progress < 100) return "Downloading ${infos.joinToString(", ")}\n${message}"
|
if (att.progress < 100) return "Downloading ${infos.joinToString(", ")}\n${message}"
|
||||||
return "${message}\nFile: ${infos.joinToString(", ")}"
|
return "${message}\nFile: ${infos.joinToString(", ")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,9 +127,10 @@ class NotificationService(val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeSetProgress(builder: NotificationCompat.Builder, progress: Int) {
|
private fun maybeSetProgress(builder: NotificationCompat.Builder, notification: Notification) {
|
||||||
|
val progress = notification.attachment?.progress
|
||||||
if (progress in 0..99) {
|
if (progress in 0..99) {
|
||||||
builder.setProgress(100, progress, false)
|
builder.setProgress(100, progress!!, false)
|
||||||
} else {
|
} else {
|
||||||
builder.setProgress(0, 0, false) // Remove progress bar
|
builder.setProgress(0, 0, false) // Remove progress bar
|
||||||
}
|
}
|
||||||
|
@ -206,10 +214,6 @@ class NotificationService(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PROGRESS_NONE = -1
|
|
||||||
const val PROGRESS_INDETERMINATE = -2
|
|
||||||
const val PROGRESS_DONE = 100
|
|
||||||
|
|
||||||
private const val TAG = "NtfyNotifService"
|
private const val TAG = "NtfyNotifService"
|
||||||
|
|
||||||
private const val CHANNEL_ID_MIN = "ntfy-min"
|
private const val CHANNEL_ID_MIN = "ntfy-min"
|
||||||
|
|
|
@ -87,8 +87,8 @@ class SubscriberConnection(
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
Log.d(TAG, "[$url] Cancelling connection")
|
Log.d(TAG, "[$url] Cancelling connection")
|
||||||
job?.cancel()
|
if (this::job.isInitialized) job?.cancel()
|
||||||
call?.cancel()
|
if (this::call.isInitialized) call?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long {
|
private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long {
|
||||||
|
|
|
@ -351,7 +351,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
Log.e(TAG, "Error fetching notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}: ${e.stackTrace}", e)
|
Log.e(TAG, "Error fetching notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}: ${e.stackTrace}", e)
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
Toast
|
Toast
|
||||||
.makeText(this@DetailActivity, getString(R.string.refresh_message_error, e.message), Toast.LENGTH_LONG)
|
.makeText(this@DetailActivity, getString(R.string.refresh_message_error_one, e.message), Toast.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
mainListContainer.isRefreshing = false
|
mainListContainer.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
package io.heckel.ntfy.ui
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.*
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.workDataOf
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.data.Notification
|
import io.heckel.ntfy.data.Notification
|
||||||
|
import io.heckel.ntfy.msg.AttachmentDownloadWorker
|
||||||
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
||||||
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
|
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
|
||||||
val selected = mutableSetOf<String>() // Notification IDs
|
val selected = mutableSetOf<String>() // Notification IDs
|
||||||
|
@ -51,13 +60,15 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
private val titleView: TextView = itemView.findViewById(R.id.detail_item_title_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 messageView: TextView = itemView.findViewById(R.id.detail_item_message_text)
|
||||||
private val newImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
|
private val newImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
|
||||||
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags)
|
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text)
|
||||||
private val imageView: ImageView = itemView.findViewById(R.id.detail_item_image)
|
private val imageView: ImageView = itemView.findViewById(R.id.detail_item_image)
|
||||||
|
private val attachmentView: TextView = itemView.findViewById(R.id.detail_item_attachment_text)
|
||||||
|
private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button)
|
||||||
|
|
||||||
fun bind(notification: Notification) {
|
fun bind(notification: Notification) {
|
||||||
this.notification = notification
|
this.notification = notification
|
||||||
|
|
||||||
val ctx = itemView.context
|
val context = itemView.context
|
||||||
val unmatchedTags = unmatchedTags(splitTags(notification.tags))
|
val unmatchedTags = unmatchedTags(splitTags(notification.tags))
|
||||||
|
|
||||||
dateView.text = Date(notification.timestamp * 1000).toString()
|
dateView.text = Date(notification.timestamp * 1000).toString()
|
||||||
|
@ -73,7 +84,7 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
}
|
}
|
||||||
if (unmatchedTags.isNotEmpty()) {
|
if (unmatchedTags.isNotEmpty()) {
|
||||||
tagsView.visibility = View.VISIBLE
|
tagsView.visibility = View.VISIBLE
|
||||||
tagsView.text = ctx.getString(R.string.detail_item_tags, unmatchedTags.joinToString(", "))
|
tagsView.text = context.getString(R.string.detail_item_tags, unmatchedTags.joinToString(", "))
|
||||||
} else {
|
} else {
|
||||||
tagsView.visibility = View.GONE
|
tagsView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
@ -83,29 +94,29 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
when (notification.priority) {
|
when (notification.priority) {
|
||||||
1 -> {
|
1 -> {
|
||||||
priorityImageView.visibility = View.VISIBLE
|
priorityImageView.visibility = View.VISIBLE
|
||||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_1_24dp))
|
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_1_24dp))
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
priorityImageView.visibility = View.VISIBLE
|
priorityImageView.visibility = View.VISIBLE
|
||||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_2_24dp))
|
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_2_24dp))
|
||||||
}
|
}
|
||||||
3 -> {
|
3 -> {
|
||||||
priorityImageView.visibility = View.GONE
|
priorityImageView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
4 -> {
|
4 -> {
|
||||||
priorityImageView.visibility = View.VISIBLE
|
priorityImageView.visibility = View.VISIBLE
|
||||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_4_24dp))
|
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_4_24dp))
|
||||||
}
|
}
|
||||||
5 -> {
|
5 -> {
|
||||||
priorityImageView.visibility = View.VISIBLE
|
priorityImageView.visibility = View.VISIBLE
|
||||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_5_24dp))
|
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_5_24dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 📄
|
|
||||||
val contentUri = notification.attachment?.contentUri
|
val contentUri = notification.attachment?.contentUri
|
||||||
if (contentUri != null && supportedImage(notification.attachment.type)) {
|
val fileExists = if (contentUri != null) fileExists(context, contentUri) else false
|
||||||
|
if (contentUri != null && fileExists && supportedImage(notification.attachment.type)) {
|
||||||
try {
|
try {
|
||||||
val resolver = itemView.context.applicationContext.contentResolver
|
val resolver = context.applicationContext.contentResolver
|
||||||
val bitmapStream = resolver.openInputStream(Uri.parse(contentUri))
|
val bitmapStream = resolver.openInputStream(Uri.parse(contentUri))
|
||||||
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
||||||
imageView.setImageBitmap(bitmap)
|
imageView.setImageBitmap(bitmap)
|
||||||
|
@ -116,6 +127,61 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
} else {
|
} else {
|
||||||
imageView.visibility = View.GONE
|
imageView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
if (notification.attachment != null) {
|
||||||
|
attachmentView.text = formatAttachmentInfo(notification, fileExists)
|
||||||
|
attachmentView.visibility = View.VISIBLE
|
||||||
|
menuButton.visibility = View.VISIBLE
|
||||||
|
menuButton.setOnClickListener { menuView ->
|
||||||
|
val popup = PopupMenu(context, menuView)
|
||||||
|
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
||||||
|
|
||||||
|
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
||||||
|
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
|
||||||
|
val browseItem = popup.menu.findItem(R.id.detail_item_menu_browse)
|
||||||
|
val copyUrlItem = popup.menu.findItem(R.id.detail_item_menu_copy_url)
|
||||||
|
if (contentUri != null && fileExists) {
|
||||||
|
openItem.setOnMenuItemClickListener {
|
||||||
|
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(contentUri))) // FIXME try/catch
|
||||||
|
true
|
||||||
|
}
|
||||||
|
browseItem.setOnMenuItemClickListener {
|
||||||
|
context.startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
copyUrlItem.setOnMenuItemClickListener {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("attachment url", notification.attachment.url)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast
|
||||||
|
.makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
downloadItem.isVisible = false
|
||||||
|
} else {
|
||||||
|
openItem.isVisible = false
|
||||||
|
browseItem.isVisible = false
|
||||||
|
downloadItem.setOnMenuItemClickListener {
|
||||||
|
scheduleAttachmentDownload(context, notification)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popup.show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attachmentView.visibility = View.GONE
|
||||||
|
menuButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleAttachmentDownload(context: Context, notification: Notification) {
|
||||||
|
Log.d(TAG, "Enqueuing work to download attachment")
|
||||||
|
val workManager = WorkManager.getInstance(context)
|
||||||
|
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
|
||||||
|
.setInputData(workDataOf("id" to notification.id))
|
||||||
|
.build()
|
||||||
|
workManager.enqueue(workRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,4 +194,8 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "NtfyDetailAdapter"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,12 +118,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
startPeriodicPollWorker()
|
startPeriodicPollWorker()
|
||||||
startPeriodicServiceRestartWorker()
|
startPeriodicServiceRestartWorker()
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
|
/*if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1234);
|
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1234);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Toast.makeText(this, "Permission already granted", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "Permission already granted", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
override fun onRequestPermissionsResult(requestCode: Int,
|
override fun onRequestPermissionsResult(requestCode: Int,
|
||||||
permissions: Array<String>,
|
permissions: Array<String>,
|
||||||
|
|
|
@ -106,6 +106,26 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto download
|
||||||
|
val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return
|
||||||
|
val autoDownload: SwitchPreference? = findPreference(autoDownloadPrefId)
|
||||||
|
autoDownload?.isChecked = repository.getAutoDownloadEnabled()
|
||||||
|
autoDownload?.preferenceDataStore = object : PreferenceDataStore() {
|
||||||
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
|
repository.setAutoDownloadEnabled(value)
|
||||||
|
}
|
||||||
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
|
return repository.getAutoDownloadEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoDownload?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
|
||||||
|
if (pref.isChecked) {
|
||||||
|
getString(R.string.settings_notifications_auto_download_summary_on)
|
||||||
|
} else {
|
||||||
|
getString(R.string.settings_notifications_auto_download_summary_off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Broadcast enabled
|
// Broadcast enabled
|
||||||
val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return
|
val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return
|
||||||
val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId)
|
val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId)
|
||||||
|
|
|
@ -2,8 +2,11 @@ package io.heckel.ntfy.util
|
||||||
|
|
||||||
import android.animation.ArgbEvaluator
|
import android.animation.ArgbEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import io.heckel.ntfy.data.Notification
|
import io.heckel.ntfy.data.Notification
|
||||||
|
import io.heckel.ntfy.data.PROGRESS_NONE
|
||||||
import io.heckel.ntfy.data.Subscription
|
import io.heckel.ntfy.data.Subscription
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
@ -106,6 +109,36 @@ fun formatTitle(notification: Notification): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME duplicate code
|
||||||
|
fun formatAttachmentInfo(notification: Notification, fileExists: Boolean): String {
|
||||||
|
if (notification.attachment == null) return ""
|
||||||
|
val att = notification.attachment
|
||||||
|
val infos = mutableListOf<String>()
|
||||||
|
if (att.name != null) infos.add(att.name)
|
||||||
|
if (att.size != null) infos.add(formatBytes(att.size))
|
||||||
|
//if (att.expires != null && att.expires != 0L) infos.add(formatDateShort(att.expires))
|
||||||
|
if (att.progress in 0..99) infos.add("${att.progress}%")
|
||||||
|
if (!fileExists) {
|
||||||
|
if (att.progress == PROGRESS_NONE) infos.add("not downloaded")
|
||||||
|
else infos.add("deleted")
|
||||||
|
}
|
||||||
|
if (infos.size == 0) return ""
|
||||||
|
if (att.progress < 100) return "Downloading ${infos.joinToString(", ")}"
|
||||||
|
return "\uD83D\uDCC4 " + infos.joinToString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
|
||||||
|
fun fileExists(context: Context, uri: String): Boolean {
|
||||||
|
val resolver = context.applicationContext.contentResolver
|
||||||
|
return try {
|
||||||
|
val fileIS = resolver.openInputStream(Uri.parse(uri))
|
||||||
|
fileIS?.close()
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
|
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
|
||||||
fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
|
fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
|
||||||
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
|
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
|
||||||
|
|
9
app/src/main/res/drawable/ic_more_horiz_gray_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_more_horiz_gray_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<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="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"
|
||||||
|
android:fillColor="#555555"/>
|
||||||
|
</vector>
|
|
@ -29,6 +29,14 @@
|
||||||
android:layout_marginTop="1dp"
|
android:layout_marginTop="1dp"
|
||||||
app:layout_constraintStart_toEndOf="@id/detail_item_priority_image"
|
app:layout_constraintStart_toEndOf="@id/detail_item_priority_image"
|
||||||
android:layout_marginStart="5dp"/>
|
android:layout_marginStart="5dp"/>
|
||||||
|
<ImageButton
|
||||||
|
android:layout_width="46dp"
|
||||||
|
android:layout_height="26dp" app:srcCompat="@drawable/ic_more_horiz_gray_24dp"
|
||||||
|
android:id="@+id/detail_item_menu_button"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="5dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground" android:paddingTop="-5dp"
|
||||||
|
android:layout_marginTop="5dp"/>
|
||||||
<TextView
|
<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: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="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -40,16 +48,6 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_title_text"
|
app:layout_constraintTop_toBottomOf="@id/detail_item_title_text"
|
||||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/detail_item_tags"/>
|
|
||||||
<TextView
|
|
||||||
android:text="Tags: ssh, zfs"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/detail_item_tags"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_message_text"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/detail_item_image"/>
|
app:layout_constraintBottom_toTopOf="@id/detail_item_image"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="This is an optional title. It can also be a little longer but not too long."
|
android:text="This is an optional title. It can also be a little longer but not too long."
|
||||||
|
@ -70,13 +68,43 @@
|
||||||
app:layout_constraintStart_toEndOf="@+id/detail_item_date_text"
|
app:layout_constraintStart_toEndOf="@+id/detail_item_date_text"
|
||||||
app:layout_constraintTop_toTopOf="@+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"/>
|
app:layout_constraintBottom_toBottomOf="@+id/detail_item_date_text" android:layout_marginStart="5dp"/>
|
||||||
<ImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
|
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
|
||||||
android:id="@+id/detail_item_image" app:layout_constraintStart_toStartOf="parent"
|
android:id="@+id/detail_item_image" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/detail_item_tags"
|
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/detail_item_message_text"
|
||||||
android:layout_marginStart="10dp" android:layout_marginEnd="10dp"
|
android:layout_marginStart="10dp" android:layout_marginEnd="10dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:adjustViewBounds="true" android:maxHeight="150dp" android:layout_marginTop="5dp"/>
|
android:adjustViewBounds="true" android:maxHeight="150dp" android:layout_marginTop="5dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_text" android:layout_marginBottom="5dp"
|
||||||
|
app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"/>
|
||||||
|
<TextView
|
||||||
|
android:text="📄attachment.jpg, 20.1 KB"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/detail_item_attachment_text"
|
||||||
|
android:textColor="@color/primaryTextColor"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
android:autoLink="web"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/detail_item_image"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/detail_item_tags_text" android:layout_marginTop="3dp"/>
|
||||||
|
<TextView
|
||||||
|
android:text="Tags: ssh, zfs"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/detail_item_tags_text"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_text"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/detail_item_padding_bottom"/>
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp" android:id="@+id/detail_item_padding_bottom"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/detail_item_tags_text"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"/>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
7
app/src/main/res/menu/menu_detail_attachment.xml
Normal file
7
app/src/main/res/menu/menu_detail_attachment.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@+id/detail_item_menu_download" android:title="@string/detail_item_menu_download"/>
|
||||||
|
<item android:id="@+id/detail_item_menu_open" android:title="@string/detail_item_menu_open"/>
|
||||||
|
<item android:id="@+id/detail_item_menu_browse" android:title="@string/detail_item_menu_browse"/>
|
||||||
|
<item android:id="@+id/detail_item_menu_copy_url" android:title="@string/detail_item_menu_copy_url"/>
|
||||||
|
</menu>
|
|
@ -24,6 +24,7 @@
|
||||||
<string name="refresh_message_result">%1$d notification(s) received</string>
|
<string name="refresh_message_result">%1$d notification(s) received</string>
|
||||||
<string name="refresh_message_no_results">Everything is up-to-date</string>
|
<string name="refresh_message_no_results">Everything is up-to-date</string>
|
||||||
<string name="refresh_message_error">%1$d subscription(s) could not be refreshed\n\n%2$s</string>
|
<string name="refresh_message_error">%1$d subscription(s) could not be refreshed\n\n%2$s</string>
|
||||||
|
<string name="refresh_message_error_one">Subscription could not be refreshed: %1$s</string>
|
||||||
|
|
||||||
<!-- Main activity: Action bar -->
|
<!-- Main activity: Action bar -->
|
||||||
<string name="main_action_bar_title">Subscribed topics</string>
|
<string name="main_action_bar_title">Subscribed topics</string>
|
||||||
|
@ -108,6 +109,11 @@
|
||||||
<string name="detail_instant_delivery_disabled">Instant delivery disabled</string>
|
<string name="detail_instant_delivery_disabled">Instant delivery disabled</string>
|
||||||
<string name="detail_instant_info">Instant delivery is enabled</string>
|
<string name="detail_instant_info">Instant delivery is enabled</string>
|
||||||
<string name="detail_item_tags">Tags: %1$s</string>
|
<string name="detail_item_tags">Tags: %1$s</string>
|
||||||
|
<string name="detail_item_menu_open">Open file</string>
|
||||||
|
<string name="detail_item_menu_browse">Browse file</string>
|
||||||
|
<string name="detail_item_menu_download">Download file</string>
|
||||||
|
<string name="detail_item_menu_copy_url">Copy URL</string>
|
||||||
|
<string name="detail_item_menu_copy_url_copied">Copied URL to clipboard</string>
|
||||||
|
|
||||||
<!-- Detail activity: Action bar -->
|
<!-- Detail activity: Action bar -->
|
||||||
<string name="detail_menu_notifications_enabled">Notifications enabled</string>
|
<string name="detail_menu_notifications_enabled">Notifications enabled</string>
|
||||||
|
@ -166,6 +172,10 @@
|
||||||
<string name="settings_notifications_min_priority_default">Default priority and higher</string>
|
<string name="settings_notifications_min_priority_default">Default priority and higher</string>
|
||||||
<string name="settings_notifications_min_priority_high">High priority and higher</string>
|
<string name="settings_notifications_min_priority_high">High priority and higher</string>
|
||||||
<string name="settings_notifications_min_priority_max">Only max priority</string>
|
<string name="settings_notifications_min_priority_max">Only max priority</string>
|
||||||
|
<string name="settings_notifications_auto_download_key">AutoDownload</string>
|
||||||
|
<string name="settings_notifications_auto_download_title">Auto download attachments</string>
|
||||||
|
<string name="settings_notifications_auto_download_summary_on">Attachments are automatically downloaded</string>
|
||||||
|
<string name="settings_notifications_auto_download_summary_off">Attachments are not automatically downloaded</string>
|
||||||
<string name="settings_unified_push_header">UnifiedPush</string>
|
<string name="settings_unified_push_header">UnifiedPush</string>
|
||||||
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
|
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
|
||||||
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>
|
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>
|
||||||
|
|
|
@ -6,4 +6,10 @@
|
||||||
<item name="android:statusBarColor">@color/primaryColor</item>
|
<item name="android:statusBarColor">@color/primaryColor</item>
|
||||||
<item name="actionModeBackground">@color/primaryDarkColor</item>
|
<item name="actionModeBackground">@color/primaryDarkColor</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Rounded corners in images: https://stackoverflow.com/a/61960983/1440785 -->
|
||||||
|
<style name="roundedCornersImageView" parent="">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSize">5dp</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -13,6 +13,10 @@
|
||||||
app:entries="@array/settings_notifications_min_priority_entries"
|
app:entries="@array/settings_notifications_min_priority_entries"
|
||||||
app:entryValues="@array/settings_notifications_min_priority_values"
|
app:entryValues="@array/settings_notifications_min_priority_values"
|
||||||
app:defaultValue="1"/>
|
app:defaultValue="1"/>
|
||||||
|
<SwitchPreference
|
||||||
|
app:key="@string/settings_notifications_auto_download_key"
|
||||||
|
app:title="@string/settings_notifications_auto_download_title"
|
||||||
|
app:enabled="true"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
app:title="@string/settings_unified_push_header"
|
app:title="@string/settings_unified_push_header"
|
||||||
|
|
|
@ -8,6 +8,7 @@ import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.data.Attachment
|
import io.heckel.ntfy.data.Attachment
|
||||||
import io.heckel.ntfy.data.Notification
|
import io.heckel.ntfy.data.Notification
|
||||||
|
import io.heckel.ntfy.data.PROGRESS_NONE
|
||||||
import io.heckel.ntfy.msg.*
|
import io.heckel.ntfy.msg.*
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
import io.heckel.ntfy.util.toPriority
|
import io.heckel.ntfy.util.toPriority
|
||||||
|
|
1
assets/more_horiz_black_24dp.svg
Normal file
1
assets/more_horiz_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"/><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
After Width: | Height: | Size: 306 B |
Loading…
Reference in a new issue