Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
dc2cfe567d
24 changed files with 557 additions and 183 deletions
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 12,
|
"version": 12,
|
||||||
"identityHash": "5a061926458ed65c80431be0a69a2450",
|
"identityHash": "d230005f4d9824ba9aa34c61003bdcbb",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "Subscription",
|
"tableName": "Subscription",
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "Notification",
|
"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, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `icon_url` TEXT, `icon_type` TEXT, `icon_size` INTEGER, `icon_contentUri` TEXT, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
|
"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, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `icon_url` TEXT, `icon_contentUri` TEXT, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -193,18 +193,6 @@
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldPath": "icon.type",
|
|
||||||
"columnName": "icon_type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "icon.size",
|
|
||||||
"columnName": "icon_size",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldPath": "icon.contentUri",
|
"fieldPath": "icon.contentUri",
|
||||||
"columnName": "icon_contentUri",
|
"columnName": "icon_contentUri",
|
||||||
|
@ -350,7 +338,7 @@
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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, '5a061926458ed65c80431be0a69a2450')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd230005f4d9824ba9aa34c61003bdcbb')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -152,8 +152,6 @@ class Backuper(val context: Context) {
|
||||||
val icon = if (n.icon != null) {
|
val icon = if (n.icon != null) {
|
||||||
io.heckel.ntfy.db.Icon(
|
io.heckel.ntfy.db.Icon(
|
||||||
url = n.icon.url,
|
url = n.icon.url,
|
||||||
type = n.icon.type,
|
|
||||||
size = n.icon.size,
|
|
||||||
contentUri = n.icon.contentUri,
|
contentUri = n.icon.contentUri,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -281,8 +279,6 @@ class Backuper(val context: Context) {
|
||||||
val icon = if (n.icon != null) {
|
val icon = if (n.icon != null) {
|
||||||
Icon(
|
Icon(
|
||||||
url = n.icon.url,
|
url = n.icon.url,
|
||||||
type = n.icon.type,
|
|
||||||
size = n.icon.size,
|
|
||||||
contentUri = n.icon.contentUri,
|
contentUri = n.icon.contentUri,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -403,10 +399,7 @@ data class Attachment(
|
||||||
|
|
||||||
data class Icon(
|
data class Icon(
|
||||||
val url: String, // URL (mandatory, see ntfy server)
|
val url: String, // URL (mandatory, see ntfy server)
|
||||||
val type: String?, // MIME type
|
|
||||||
val size: Long?, // Size in bytes
|
|
||||||
val contentUri: String?, // After it's downloaded, the content:// location
|
val contentUri: String?, // After it's downloaded, the content:// location
|
||||||
val progress: Int, // Progress during download, -1 if not downloaded
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class User(
|
data class User(
|
||||||
|
|
|
@ -95,12 +95,10 @@ const val ATTACHMENT_PROGRESS_DONE = 100
|
||||||
@Entity
|
@Entity
|
||||||
data class Icon(
|
data class Icon(
|
||||||
@ColumnInfo(name = "url") val url: String, // URL (mandatory, see ntfy server)
|
@ColumnInfo(name = "url") val url: String, // URL (mandatory, see ntfy server)
|
||||||
@ColumnInfo(name = "type") val type: String?, // MIME type
|
|
||||||
@ColumnInfo(name = "size") val size: Long?, // Size in bytes
|
|
||||||
@ColumnInfo(name = "contentUri") val contentUri: String?, // After it's downloaded, the content:// location
|
@ColumnInfo(name = "contentUri") val contentUri: String?, // After it's downloaded, the content:// location
|
||||||
) {
|
) {
|
||||||
constructor(url:String, type: String?, size: Long?) :
|
constructor(url:String) :
|
||||||
this(url, type, size, null)
|
this(url, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -282,8 +280,6 @@ abstract class Database : RoomDatabase() {
|
||||||
db.execSQL("ALTER TABLE Subscription ADD COLUMN lastNotificationId TEXT")
|
db.execSQL("ALTER TABLE Subscription ADD COLUMN lastNotificationId TEXT")
|
||||||
db.execSQL("ALTER TABLE Subscription ADD COLUMN displayName TEXT")
|
db.execSQL("ALTER TABLE Subscription ADD COLUMN displayName TEXT")
|
||||||
db.execSQL("ALTER TABLE Notification ADD COLUMN icon_url TEXT") // Room limitation: Has to be nullable for @Embedded
|
db.execSQL("ALTER TABLE Notification ADD COLUMN icon_url TEXT") // Room limitation: Has to be nullable for @Embedded
|
||||||
db.execSQL("ALTER TABLE Notification ADD COLUMN icon_type TEXT")
|
|
||||||
db.execSQL("ALTER TABLE Notification ADD COLUMN icon_size INT")
|
|
||||||
db.execSQL("ALTER TABLE Notification ADD COLUMN icon_contentUri TEXT")
|
db.execSQL("ALTER TABLE Notification ADD COLUMN icon_contentUri TEXT")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,8 +380,11 @@ interface NotificationDao {
|
||||||
@Query("SELECT * FROM notification WHERE deleted = 1 AND attachment_contentUri <> ''")
|
@Query("SELECT * FROM notification WHERE deleted = 1 AND attachment_contentUri <> ''")
|
||||||
fun listDeletedWithAttachments(): List<Notification>
|
fun listDeletedWithAttachments(): List<Notification>
|
||||||
|
|
||||||
@Query("SELECT * FROM notification WHERE deleted = 1 AND icon_contentUri <> ''")
|
@Query("SELECT DISTINCT icon_contentUri FROM notification WHERE deleted != 1 AND icon_contentUri <> ''")
|
||||||
fun listDeletedWithIcons(): List<Notification>
|
fun listActiveIconUris(): List<String>
|
||||||
|
|
||||||
|
@Query("UPDATE notification SET icon_contentUri = null WHERE icon_contentUri = :uri")
|
||||||
|
fun clearIconUri(uri: String)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
fun add(notification: Notification)
|
fun add(notification: Notification)
|
||||||
|
|
|
@ -92,8 +92,12 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
return notificationDao.listDeletedWithAttachments()
|
return notificationDao.listDeletedWithAttachments()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDeletedNotificationsWithIcons(): List<Notification> {
|
fun getActiveIconUris(): Set<String> {
|
||||||
return notificationDao.listDeletedWithIcons()
|
return notificationDao.listActiveIconUris().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearIconUri(uri: String) {
|
||||||
|
notificationDao.clearIconUri(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
|
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
|
||||||
|
|
|
@ -82,11 +82,7 @@ class DownloadAttachmentWorker(private val context: Context, params: WorkerParam
|
||||||
Log.d(TAG, "Starting download to content URI: $uri")
|
Log.d(TAG, "Starting download to content URI: $uri")
|
||||||
var bytesCopied: Long = 0
|
var bytesCopied: Long = 0
|
||||||
val outFile = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
val outFile = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
||||||
val downloadLimit = if (repository.getAutoDownloadMaxSize() != Repository.AUTO_DOWNLOAD_NEVER && repository.getAutoDownloadMaxSize() != Repository.AUTO_DOWNLOAD_ALWAYS) {
|
val downloadLimit = getDownloadLimit(userAction)
|
||||||
repository.getAutoDownloadMaxSize()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
outFile.use { fileOut ->
|
outFile.use { fileOut ->
|
||||||
val fileIn = response.body!!.byteStream()
|
val fileIn = response.body!!.byteStream()
|
||||||
val buffer = ByteArray(BUFFER_SIZE)
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
|
@ -192,6 +188,14 @@ class DownloadAttachmentWorker(private val context: Context, params: WorkerParam
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDownloadLimit(userAction: Boolean): Long? {
|
||||||
|
return if (userAction || repository.getAutoDownloadMaxSize() == Repository.AUTO_DOWNLOAD_ALWAYS) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
repository.getAutoDownloadMaxSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createUri(notification: Notification): Uri {
|
private fun createUri(notification: Notification): Uri {
|
||||||
val attachmentDir = File(context.cacheDir, ATTACHMENT_CACHE_DIR)
|
val attachmentDir = File(context.cacheDir, ATTACHMENT_CACHE_DIR)
|
||||||
if (!attachmentDir.exists() && !attachmentDir.mkdirs()) {
|
if (!attachmentDir.exists() && !attachmentDir.mkdirs()) {
|
||||||
|
|
|
@ -2,28 +2,24 @@ package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import io.heckel.ntfy.BuildConfig
|
import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.R
|
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.*
|
import io.heckel.ntfy.db.*
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.util.ensureSafeNewFile
|
import io.heckel.ntfy.util.sha256
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class DownloadIconWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
|
class DownloadIconWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
private val client = OkHttpClient.Builder()
|
private val client = OkHttpClient.Builder()
|
||||||
.callTimeout(15, TimeUnit.MINUTES) // Total timeout for entire request
|
.callTimeout(1, TimeUnit.MINUTES) // Total timeout for entire request
|
||||||
.connectTimeout(15, TimeUnit.SECONDS)
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
.readTimeout(15, TimeUnit.SECONDS)
|
.readTimeout(15, TimeUnit.SECONDS)
|
||||||
.writeTimeout(15, TimeUnit.SECONDS)
|
.writeTimeout(15, TimeUnit.SECONDS)
|
||||||
|
@ -44,7 +40,16 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
|
||||||
subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
||||||
icon = notification.icon ?: return Result.failure()
|
icon = notification.icon ?: return Result.failure()
|
||||||
try {
|
try {
|
||||||
downloadIcon()
|
val iconFile = createIconFile(icon)
|
||||||
|
val yesterdayTimestamp = Date().time - MAX_CACHE_MILLIS
|
||||||
|
if (!iconFile.exists() || iconFile.lastModified() < yesterdayTimestamp) {
|
||||||
|
downloadIcon(iconFile)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Loading icon from cache: $iconFile")
|
||||||
|
val iconUri = createIconUri(iconFile)
|
||||||
|
this.uri = iconUri // Required for cleanup in onStopped()
|
||||||
|
save(icon.copy(contentUri = iconUri.toString()))
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
failed(e)
|
failed(e)
|
||||||
}
|
}
|
||||||
|
@ -56,43 +61,35 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
|
||||||
maybeDeleteFile()
|
maybeDeleteFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadIcon() {
|
private fun downloadIcon(iconFile: File) {
|
||||||
Log.d(TAG, "Downloading icon from ${icon.url}")
|
Log.d(TAG, "Downloading icon from ${icon.url}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(icon.url)
|
.url(icon.url)
|
||||||
.addHeader("User-Agent", ApiService.USER_AGENT)
|
.addHeader("User-Agent", ApiService.USER_AGENT)
|
||||||
.build()
|
.build()
|
||||||
client.newCall(request).execute().use { response ->
|
client.newCall(request).execute().use { response ->
|
||||||
Log.d(TAG, "Download: headers received: $response")
|
Log.d(TAG, "Headers received: $response, Content-Length: ${response.headers["Content-Length"]}")
|
||||||
if (!response.isSuccessful || response.body == null) {
|
if (!response.isSuccessful || response.body == null) {
|
||||||
throw Exception("Unexpected response: ${response.code}")
|
throw Exception("Unexpected response: ${response.code}")
|
||||||
}
|
} else if (shouldAbortDownload(response)) {
|
||||||
save(updateIconFromResponse(response))
|
|
||||||
if (shouldAbortDownload()) {
|
|
||||||
Log.d(TAG, "Aborting download: Content-Length is larger than auto-download setting")
|
Log.d(TAG, "Aborting download: Content-Length is larger than auto-download setting")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val resolver = applicationContext.contentResolver
|
val resolver = applicationContext.contentResolver
|
||||||
val uri = createUri(notification)
|
val uri = createIconUri(iconFile)
|
||||||
this.uri = uri // Required for cleanup in onStopped()
|
this.uri = uri // Required for cleanup in onStopped()
|
||||||
|
|
||||||
Log.d(TAG, "Starting download to content URI: $uri")
|
Log.d(TAG, "Starting download to content URI: $uri")
|
||||||
val contentLength = response.headers["Content-Length"]?.toLongOrNull()
|
|
||||||
var bytesCopied: Long = 0
|
var bytesCopied: Long = 0
|
||||||
val outFile = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
val outFile = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
||||||
val downloadLimit = if (repository.getAutoDownloadMaxSize() != Repository.AUTO_DOWNLOAD_NEVER && repository.getAutoDownloadMaxSize() != Repository.AUTO_DOWNLOAD_ALWAYS) {
|
val downloadLimit = getDownloadLimit()
|
||||||
repository.getAutoDownloadMaxSize()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
outFile.use { fileOut ->
|
outFile.use { fileOut ->
|
||||||
val fileIn = response.body!!.byteStream()
|
val fileIn = response.body!!.byteStream()
|
||||||
val buffer = ByteArray(BUFFER_SIZE)
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
var bytes = fileIn.read(buffer)
|
var bytes = fileIn.read(buffer)
|
||||||
while (bytes >= 0) {
|
while (bytes >= 0) {
|
||||||
if (downloadLimit != null && bytesCopied > downloadLimit) {
|
if (bytesCopied > downloadLimit) {
|
||||||
throw Exception("Icon is longer than max download size.")
|
throw Exception("Icon is longer than max download size.")
|
||||||
}
|
}
|
||||||
fileOut.write(buffer, 0, bytes)
|
fileOut.write(buffer, 0, bytes)
|
||||||
|
@ -101,49 +98,13 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Icon download: successful response, proceeding with download")
|
Log.d(TAG, "Icon download: successful response, proceeding with download")
|
||||||
save(icon.copy(
|
save(icon.copy(contentUri = uri.toString()))
|
||||||
size = bytesCopied,
|
|
||||||
contentUri = uri.toString()
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
failed(e)
|
failed(e)
|
||||||
|
|
||||||
// Toast in a Worker: https://stackoverflow.com/a/56428145/1440785
|
|
||||||
val handler = Handler(Looper.getMainLooper())
|
|
||||||
handler.postDelayed({
|
|
||||||
Toast
|
|
||||||
.makeText(context, context.getString(R.string.detail_item_icon_download_failed, e.message), Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}, 200)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateIconFromResponse(response: Response): Icon {
|
|
||||||
val size = if (response.headers["Content-Length"]?.toLongOrNull() != null) {
|
|
||||||
Log.d(TAG, "We got the long! icon here")
|
|
||||||
response.headers["Content-Length"]?.toLong()
|
|
||||||
} else {
|
|
||||||
icon.size // May be null!
|
|
||||||
}
|
|
||||||
val mimeType = if (response.headers["Content-Type"] != null) {
|
|
||||||
response.headers["Content-Type"]
|
|
||||||
} else {
|
|
||||||
val ext = MimeTypeMap.getFileExtensionFromUrl(icon.url)
|
|
||||||
if (ext != null) {
|
|
||||||
val typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
|
|
||||||
typeFromExt ?: icon.type // May be null!
|
|
||||||
} else {
|
|
||||||
icon.type // May be null!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(TAG, "New icon size: $size, type: $mimeType")
|
|
||||||
return icon.copy(
|
|
||||||
size = size,
|
|
||||||
type = mimeType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun failed(e: Exception) {
|
private fun failed(e: Exception) {
|
||||||
Log.w(TAG, "Icon download failed", e)
|
Log.w(TAG, "Icon download failed", e)
|
||||||
maybeDeleteFile()
|
maybeDeleteFile()
|
||||||
|
@ -166,28 +127,42 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
|
||||||
repository.updateNotification(notification)
|
repository.updateNotification(notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldAbortDownload(): Boolean {
|
private fun shouldAbortDownload(response: Response): Boolean {
|
||||||
val maxAutoDownloadSize = MAX_ICON_DOWNLOAD_SIZE
|
val maxAutoDownloadSize = getDownloadLimit()
|
||||||
val size = icon.size ?: return false // Don't abort if size unknown
|
val size = response.headers["Content-Length"]?.toLongOrNull() ?: return false // Don't abort here if size unknown
|
||||||
return size > maxAutoDownloadSize
|
return size > maxAutoDownloadSize
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUri(notification: Notification): Uri {
|
private fun getDownloadLimit(): Long {
|
||||||
|
return if (repository.getAutoDownloadMaxSize() != Repository.AUTO_DOWNLOAD_NEVER && repository.getAutoDownloadMaxSize() != Repository.AUTO_DOWNLOAD_ALWAYS) {
|
||||||
|
Math.min(repository.getAutoDownloadMaxSize(), MAX_ICON_DOWNLOAD_BYTES)
|
||||||
|
} else {
|
||||||
|
DEFAULT_MAX_ICON_DOWNLOAD_BYTES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIconFile(icon: Icon): File {
|
||||||
val iconDir = File(context.cacheDir, ICON_CACHE_DIR)
|
val iconDir = File(context.cacheDir, ICON_CACHE_DIR)
|
||||||
if (!iconDir.exists() && !iconDir.mkdirs()) {
|
if (!iconDir.exists() && !iconDir.mkdirs()) {
|
||||||
throw Exception("Cannot create cache directory for icons: $iconDir")
|
throw Exception("Cannot create cache directory for icons: $iconDir")
|
||||||
}
|
}
|
||||||
val file = ensureSafeNewFile(iconDir, notification.id)
|
val hash = icon.url.sha256()
|
||||||
return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file)
|
return File(iconDir, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIconUri(iconFile: File): Uri {
|
||||||
|
return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, iconFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val INPUT_DATA_ID = "id"
|
const val INPUT_DATA_ID = "id"
|
||||||
const val FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider" // See AndroidManifest.xml
|
const val FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider" // See AndroidManifest.xml
|
||||||
const val MAX_ICON_DOWNLOAD_SIZE = 300000
|
const val DEFAULT_MAX_ICON_DOWNLOAD_BYTES = 307_200L // 300 KB
|
||||||
|
const val MAX_ICON_DOWNLOAD_BYTES = 5_242_880L // 5 MB
|
||||||
|
const val MAX_CACHE_MILLIS = 1000*60*60*24 // 24 hours
|
||||||
|
const val ICON_CACHE_DIR = "icons"
|
||||||
|
|
||||||
private const val TAG = "NtfyIconDownload"
|
private const val TAG = "NtfyIconDownload"
|
||||||
private const val ICON_CACHE_DIR = "icons"
|
|
||||||
private const val BUFFER_SIZE = 8 * 1024
|
private const val BUFFER_SIZE = 8 * 1024
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ data class Message(
|
||||||
val priority: Int?,
|
val priority: Int?,
|
||||||
val tags: List<String>?,
|
val tags: List<String>?,
|
||||||
val click: String?,
|
val click: String?,
|
||||||
val icon: MessageIcon?,
|
val icon: String?,
|
||||||
val actions: List<MessageAction>?,
|
val actions: List<MessageAction>?,
|
||||||
val title: String?,
|
val title: String?,
|
||||||
val message: String,
|
val message: String,
|
||||||
|
|
|
@ -60,8 +60,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
||||||
Log.d(TAG, "Attachment already expired at ${attachment.expires}, not downloading")
|
Log.d(TAG, "Attachment already expired at ${attachment.expires}, not downloading")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val maxAutoDownloadSize = repository.getAutoDownloadMaxSize()
|
when (val maxAutoDownloadSize = repository.getAutoDownloadMaxSize()) {
|
||||||
when (maxAutoDownloadSize) {
|
|
||||||
Repository.AUTO_DOWNLOAD_ALWAYS -> return true
|
Repository.AUTO_DOWNLOAD_ALWAYS -> return true
|
||||||
Repository.AUTO_DOWNLOAD_NEVER -> return false
|
Repository.AUTO_DOWNLOAD_NEVER -> return false
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -73,15 +72,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun shouldDownloadIcon(notification: Notification): Boolean {
|
private fun shouldDownloadIcon(notification: Notification): Boolean {
|
||||||
if (notification.icon == null) {
|
return notification.icon != null
|
||||||
return false
|
|
||||||
}
|
|
||||||
val icon = notification.icon
|
|
||||||
val maxIconDownloadSize = DownloadIconWorker.MAX_ICON_DOWNLOAD_SIZE
|
|
||||||
if (icon.size == null) {
|
|
||||||
return true // DownloadWorker will bail out if attachment is too large!
|
|
||||||
}
|
|
||||||
return icon.size <= maxIconDownloadSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
|
private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
|
||||||
|
|
|
@ -57,6 +57,7 @@ class NotificationParser {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
|
val icon: Icon? = if (message.icon != null) Icon(url = message.icon) else null
|
||||||
val notification = Notification(
|
val notification = Notification(
|
||||||
id = message.id,
|
id = message.id,
|
||||||
subscriptionId = subscriptionId,
|
subscriptionId = subscriptionId,
|
||||||
|
|
|
@ -96,7 +96,7 @@ class NotificationService(val context: Context) {
|
||||||
val contentUri = notification.attachment?.contentUri
|
val contentUri = notification.attachment?.contentUri
|
||||||
val isSupportedImage = supportedImage(notification.attachment?.type)
|
val isSupportedImage = supportedImage(notification.attachment?.type)
|
||||||
val subscriptionIcon = if (subscription.icon != null) subscription.icon.readBitmapFromUriOrNull(context) else null
|
val subscriptionIcon = if (subscription.icon != null) subscription.icon.readBitmapFromUriOrNull(context) else null
|
||||||
val notificationIcon = if (notification.icon != null && supportedImage(notification.icon.type)) notification.icon.contentUri?.readBitmapFromUriOrNull(context) else null
|
val notificationIcon = if (notification.icon != null) notification.icon.contentUri?.readBitmapFromUriOrNull(context) else null
|
||||||
val largeIcon = notificationIcon ?: subscriptionIcon
|
val largeIcon = notificationIcon ?: subscriptionIcon
|
||||||
if (contentUri != null && isSupportedImage) {
|
if (contentUri != null && isSupportedImage) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -80,6 +80,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text)
|
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 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 iconView: ImageView = itemView.findViewById(R.id.detail_item_icon)
|
||||||
private val newDotImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
|
private val newDotImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
|
||||||
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text)
|
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text)
|
||||||
private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button)
|
private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button)
|
||||||
|
@ -130,11 +131,13 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
cardView.setCardBackgroundColor(Colors.cardBackgroundColor(context))
|
cardView.setCardBackgroundColor(Colors.cardBackgroundColor(context))
|
||||||
}
|
}
|
||||||
val attachment = notification.attachment
|
val attachment = notification.attachment
|
||||||
val exists = if (attachment?.contentUri != null) fileExists(context, attachment.contentUri) else false
|
val attachmentFileStat = maybeFileStat(context, attachment?.contentUri)
|
||||||
|
val iconFileStat = maybeFileStat(context, notification.icon?.contentUri)
|
||||||
renderPriority(context, notification)
|
renderPriority(context, notification)
|
||||||
resetCardButtons()
|
resetCardButtons()
|
||||||
maybeRenderMenu(context, notification, exists)
|
maybeRenderMenu(context, notification, attachmentFileStat)
|
||||||
maybeRenderAttachment(context, notification, exists)
|
maybeRenderAttachment(context, notification, attachmentFileStat)
|
||||||
|
maybeRenderIcon(context, notification, iconFileStat)
|
||||||
maybeRenderActions(context, notification)
|
maybeRenderActions(context, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,20 +165,35 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRenderAttachment(context: Context, notification: Notification, exists: Boolean) {
|
private fun maybeRenderAttachment(context: Context, notification: Notification, attachmentFileStat: FileInfo?) {
|
||||||
if (notification.attachment == null) {
|
if (notification.attachment == null) {
|
||||||
attachmentImageView.visibility = View.GONE
|
attachmentImageView.visibility = View.GONE
|
||||||
attachmentBoxView.visibility = View.GONE
|
attachmentBoxView.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val attachment = notification.attachment
|
val attachment = notification.attachment
|
||||||
val image = attachment.contentUri != null && exists && supportedImage(attachment.type)
|
val image = attachment.contentUri != null && supportedImage(attachment.type) && previewableImage(attachmentFileStat)
|
||||||
maybeRenderAttachmentImage(context, attachment, image)
|
maybeRenderAttachmentImage(context, attachment, image)
|
||||||
maybeRenderAttachmentBox(context, notification, attachment, exists, image)
|
maybeRenderAttachmentBox(context, notification, attachment, attachmentFileStat, image)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRenderMenu(context: Context, notification: Notification, exists: Boolean) {
|
private fun maybeRenderIcon(context: Context, notification: Notification, iconStat: FileInfo?) {
|
||||||
val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, exists) // Heavy lifting not during on-click
|
if (notification.icon == null || !previewableImage(iconStat)) {
|
||||||
|
iconView.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val icon = notification.icon
|
||||||
|
val bitmap = icon.contentUri?.readBitmapFromUri(context) ?: throw Exception("uri empty")
|
||||||
|
iconView.setImageBitmap(bitmap)
|
||||||
|
iconView.visibility = View.VISIBLE
|
||||||
|
} catch (_: Exception) {
|
||||||
|
iconView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeRenderMenu(context: Context, notification: Notification, attachmentFileStat: FileInfo?) {
|
||||||
|
val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, attachmentFileStat) // Heavy lifting not during on-click
|
||||||
if (menuButtonPopupMenu != null) {
|
if (menuButtonPopupMenu != null) {
|
||||||
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
||||||
menuButton.visibility = View.VISIBLE
|
menuButton.visibility = View.VISIBLE
|
||||||
|
@ -220,14 +238,14 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
return button
|
return button
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRenderAttachmentBox(context: Context, notification: Notification, attachment: Attachment, exists: Boolean, image: Boolean) {
|
private fun maybeRenderAttachmentBox(context: Context, notification: Notification, attachment: Attachment, attachmentFileStat: FileInfo?, image: Boolean) {
|
||||||
if (image) {
|
if (image) {
|
||||||
attachmentBoxView.visibility = View.GONE
|
attachmentBoxView.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
|
attachmentInfoView.text = formatAttachmentDetails(context, attachment, attachmentFileStat)
|
||||||
attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type))
|
attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type))
|
||||||
val attachmentBoxPopupMenu = maybeCreateMenuPopup(context, attachmentBoxView, notification, exists) // Heavy lifting not during on-click
|
val attachmentBoxPopupMenu = maybeCreateMenuPopup(context, attachmentBoxView, notification, attachmentFileStat) // Heavy lifting not during on-click
|
||||||
if (attachmentBoxPopupMenu != null) {
|
if (attachmentBoxPopupMenu != null) {
|
||||||
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
||||||
} else {
|
} else {
|
||||||
|
@ -240,11 +258,12 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
attachmentBoxView.visibility = View.VISIBLE
|
attachmentBoxView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeCreateMenuPopup(context: Context, anchor: View?, notification: Notification, exists: Boolean): PopupMenu? {
|
private fun maybeCreateMenuPopup(context: Context, anchor: View?, notification: Notification, attachmentFileStat: FileInfo?): PopupMenu? {
|
||||||
val popup = PopupMenu(context, anchor)
|
val popup = PopupMenu(context, anchor)
|
||||||
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
||||||
val attachment = notification.attachment // May be null
|
val attachment = notification.attachment // May be null
|
||||||
val hasAttachment = attachment != null
|
val hasAttachment = attachment != null
|
||||||
|
val attachmentExists = attachmentFileStat != null
|
||||||
val hasClickLink = notification.click != ""
|
val hasClickLink = notification.click != ""
|
||||||
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
||||||
val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
|
val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
|
||||||
|
@ -266,10 +285,10 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
if (hasClickLink) {
|
if (hasClickLink) {
|
||||||
copyContentsItem.setOnMenuItemClickListener { copyContents(context, notification) }
|
copyContentsItem.setOnMenuItemClickListener { copyContents(context, notification) }
|
||||||
}
|
}
|
||||||
openItem.isVisible = hasAttachment && exists
|
openItem.isVisible = hasAttachment && attachmentExists
|
||||||
downloadItem.isVisible = hasAttachment && !exists && !expired && !inProgress
|
downloadItem.isVisible = hasAttachment && !attachmentExists && !expired && !inProgress
|
||||||
deleteItem.isVisible = hasAttachment && exists
|
deleteItem.isVisible = hasAttachment && attachmentExists
|
||||||
saveFileItem.isVisible = hasAttachment && exists
|
saveFileItem.isVisible = hasAttachment && attachmentExists
|
||||||
copyUrlItem.isVisible = hasAttachment && !expired
|
copyUrlItem.isVisible = hasAttachment && !expired
|
||||||
cancelItem.isVisible = hasAttachment && inProgress
|
cancelItem.isVisible = hasAttachment && inProgress
|
||||||
copyContentsItem.isVisible = notification.click != ""
|
copyContentsItem.isVisible = notification.click != ""
|
||||||
|
@ -282,8 +301,9 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
return popup
|
return popup
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatAttachmentDetails(context: Context, attachment: Attachment, exists: Boolean): String {
|
private fun formatAttachmentDetails(context: Context, attachment: Attachment, attachmentFileStat: FileInfo?): String {
|
||||||
val name = attachment.name
|
val name = attachment.name
|
||||||
|
val exists = attachmentFileStat != null
|
||||||
val notYetDownloaded = !exists && attachment.progress == ATTACHMENT_PROGRESS_NONE
|
val notYetDownloaded = !exists && attachment.progress == ATTACHMENT_PROGRESS_NONE
|
||||||
val downloading = !exists && attachment.progress in 0..99
|
val downloading = !exists && attachment.progress in 0..99
|
||||||
val deleted = !exists && (attachment.progress == ATTACHMENT_PROGRESS_DONE || attachment.progress == ATTACHMENT_PROGRESS_DELETED)
|
val deleted = !exists && (attachment.progress == ATTACHMENT_PROGRESS_DONE || attachment.progress == ATTACHMENT_PROGRESS_DELETED)
|
||||||
|
@ -499,6 +519,10 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
}
|
}
|
||||||
context.sendBroadcast(intent)
|
context.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun previewableImage(fileStat: FileInfo?): Boolean {
|
||||||
|
return if (fileStat != null) fileStat.size <= IMAGE_PREVIEW_MAX_BYTES else false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object TopicDiffCallback : DiffUtil.ItemCallback<Notification>() {
|
object TopicDiffCallback : DiffUtil.ItemCallback<Notification>() {
|
||||||
|
@ -514,5 +538,6 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "NtfyDetailAdapter"
|
const val TAG = "NtfyDetailAdapter"
|
||||||
const val REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD = 9876
|
const val REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD = 9876
|
||||||
|
const val IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024 // Too large images crash the app with "Canvas: trying to draw too large(233280000bytes) bitmap."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,8 @@ import io.heckel.ntfy.db.Subscription
|
||||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
|
import io.heckel.ntfy.msg.DownloadManager
|
||||||
|
import io.heckel.ntfy.msg.DownloadType
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||||
|
@ -456,7 +458,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
try {
|
try {
|
||||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
|
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
|
||||||
notifications.forEach { notification -> repository.addNotification(notification) }
|
notifications.forEach { notification ->
|
||||||
|
repository.addNotification(notification)
|
||||||
|
if (notification.icon != null) {
|
||||||
|
DownloadManager.enqueue(this@MainActivity, notification.id, userAction = false, DownloadType.ICON)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to fetch notifications: ${e.message}", e)
|
Log.e(TAG, "Unable to fetch notifications: ${e.message}", e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,10 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.io.*
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.StringCharacterIterator
|
import java.text.StringCharacterIterator
|
||||||
|
@ -259,6 +262,14 @@ fun fileStat(context: Context, contentUri: Uri?): FileInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun maybeFileStat(context: Context, contentUri: String?): FileInfo? {
|
||||||
|
return try {
|
||||||
|
fileStat(context, Uri.parse(contentUri)) // Throws if the file does not exist
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class FileInfo(
|
data class FileInfo(
|
||||||
val filename: String,
|
val filename: String,
|
||||||
val size: Long,
|
val size: Long,
|
||||||
|
@ -469,3 +480,9 @@ fun copyToClipboard(context: Context, notification: Notification) {
|
||||||
.makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
.makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.sha256(): String {
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
val digest = md.digest(this.toByteArray())
|
||||||
|
return digest.fold("") { str, it -> str + "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
|
@ -2,16 +2,21 @@ package io.heckel.ntfy.work
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import io.heckel.ntfy.BuildConfig
|
import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.db.ATTACHMENT_PROGRESS_DELETED
|
import io.heckel.ntfy.db.ATTACHMENT_PROGRESS_DELETED
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
|
import io.heckel.ntfy.msg.DownloadIconWorker
|
||||||
import io.heckel.ntfy.ui.DetailAdapter
|
import io.heckel.ntfy.ui.DetailAdapter
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
|
import io.heckel.ntfy.util.fileStat
|
||||||
import io.heckel.ntfy.util.topicShortUrl
|
import io.heckel.ntfy.util.topicShortUrl
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes notifications marked for deletion and attachments for deleted notifications.
|
* Deletes notifications marked for deletion and attachments for deleted notifications.
|
||||||
|
@ -62,25 +67,24 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
|
||||||
|
|
||||||
private fun deleteExpiredIcons() {
|
private fun deleteExpiredIcons() {
|
||||||
Log.d(TAG, "Deleting icons for deleted notifications")
|
Log.d(TAG, "Deleting icons for deleted notifications")
|
||||||
val resolver = applicationContext.contentResolver
|
|
||||||
val repository = Repository.getInstance(applicationContext)
|
val repository = Repository.getInstance(applicationContext)
|
||||||
val notifications = repository.getDeletedNotificationsWithIcons()
|
val activeIconUris = repository.getActiveIconUris()
|
||||||
notifications.forEach { notification ->
|
val activeIconFilenames = activeIconUris.map{ fileStat(applicationContext, Uri.parse(it)).filename }.toSet()
|
||||||
|
val iconDir = File(applicationContext.cacheDir, DownloadIconWorker.ICON_CACHE_DIR)
|
||||||
|
val allIconFilenames = iconDir.listFiles()?.map{ file -> file.name }.orEmpty()
|
||||||
|
val filenamesToDelete = allIconFilenames.minus(activeIconFilenames)
|
||||||
|
filenamesToDelete.forEach { filename ->
|
||||||
try {
|
try {
|
||||||
val icon = notification.icon ?: return
|
val file = File(iconDir, filename)
|
||||||
val contentUri = Uri.parse(icon.contentUri ?: return)
|
val deleted = file.delete()
|
||||||
Log.d(TAG, "Deleting icon for notification ${notification.id}: ${icon.contentUri} (${icon.url})")
|
|
||||||
val deleted = resolver.delete(contentUri, null, null) > 0
|
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
Log.w(TAG, "Unable to delete icon for notification ${notification.id}")
|
Log.w(TAG, "Unable to delete icon: $filename")
|
||||||
}
|
}
|
||||||
val newIcon = icon.copy(
|
val uri = FileProvider.getUriForFile(applicationContext,
|
||||||
contentUri = null,
|
DownloadIconWorker.FILE_PROVIDER_AUTHORITY, file).toString()
|
||||||
)
|
repository.clearIconUri(uri)
|
||||||
val newNotification = notification.copy(icon = newIcon)
|
|
||||||
repository.updateNotification(newNotification)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to delete icon for notification: ${e.message}", e)
|
Log.w(TAG, "Failed to delete icon: ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +114,6 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
|
||||||
val deleteOlderThanTimestamp = (System.currentTimeMillis()/1000) - HARD_DELETE_AFTER_SECONDS
|
val deleteOlderThanTimestamp = (System.currentTimeMillis()/1000) - HARD_DELETE_AFTER_SECONDS
|
||||||
Log.d(TAG, "[$logId] Hard deleting notifications older than $markDeletedOlderThanTimestamp")
|
Log.d(TAG, "[$logId] Hard deleting notifications older than $markDeletedOlderThanTimestamp")
|
||||||
repository.removeNotificationsIfOlderThan(subscription.id, deleteOlderThanTimestamp)
|
repository.removeNotificationsIfOlderThan(subscription.id, deleteOlderThanTimestamp)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:paddingBottom="6dp" android:paddingTop="6dp">
|
android:paddingBottom="6dp" android:paddingTop="6dp" android:paddingEnd="6dp">
|
||||||
<TextView
|
<TextView
|
||||||
android:text="Sun, October 31, 2021, 10:43:12"
|
android:text="Sun, October 31, 2021, 10:43:12"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -47,13 +47,12 @@
|
||||||
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
|
<ImageButton
|
||||||
android:layout_width="46dp"
|
android:layout_width="28dp"
|
||||||
android:layout_height="26dp" app:srcCompat="@drawable/ic_more_horiz_gray_24dp"
|
android:layout_height="26dp" app:srcCompat="@drawable/ic_more_horiz_gray_24dp"
|
||||||
android:id="@+id/detail_item_menu_button"
|
android:id="@+id/detail_item_menu_button"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="7dp"
|
|
||||||
android:background="?android:attr/selectableItemBackground" android:paddingTop="-5dp"
|
android:background="?android:attr/selectableItemBackground" android:paddingTop="-5dp"
|
||||||
/>
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="3dp"/>
|
||||||
<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"
|
||||||
|
@ -64,8 +63,7 @@
|
||||||
android:autoLink="web"
|
android:autoLink="web"
|
||||||
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="12dp"
|
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="12dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="12dp"
|
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_image" app:layout_constraintEnd_toStartOf="@id/detail_item_icon" android:layout_marginEnd="6dp"/>
|
||||||
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_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."
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -74,10 +72,9 @@
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||||
android:autoLink="web"
|
android:autoLink="web"
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="12dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_marginStart="12dp" android:textStyle="bold"
|
android:layout_marginStart="12dp" android:textStyle="bold"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/detail_item_date_text"/>
|
app:layout_constraintTop_toBottomOf="@+id/detail_item_date_text" app:layout_constraintEnd_toStartOf="@id/detail_item_icon" android:layout_marginEnd="6dp" tools:layout_constraintEnd_toStartOf="@id/detail_item_icon"/>
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="16dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="16dp" app:srcCompat="@drawable/ic_priority_5_24dp"
|
android:layout_height="16dp" app:srcCompat="@drawable/ic_priority_5_24dp"
|
||||||
|
@ -90,9 +87,9 @@
|
||||||
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_attachment_image" app:layout_constraintStart_toStartOf="parent"
|
android:id="@+id/detail_item_attachment_image" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/detail_item_message_text"
|
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/detail_item_message_text"
|
||||||
android:layout_marginStart="12dp" android:layout_marginEnd="12dp"
|
android:layout_marginStart="12dp" android:layout_marginEnd="6dp"
|
||||||
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="7dp"
|
||||||
app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"
|
app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"
|
||||||
android:layout_marginBottom="3dp" app:layout_constraintBottom_toTopOf="@id/detail_item_tags_text"/>
|
android:layout_marginBottom="3dp" app:layout_constraintBottom_toTopOf="@id/detail_item_tags_text"/>
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -102,7 +99,7 @@
|
||||||
android:id="@+id/detail_item_tags_text"
|
android:id="@+id/detail_item_tags_text"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="12dp"
|
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="12dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="12dp"
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="6dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_image"
|
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_image"
|
||||||
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_file_box"
|
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_file_box"
|
||||||
app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="2dp"
|
app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="2dp"
|
||||||
|
@ -111,7 +108,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"
|
android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"
|
||||||
android:id="@+id/detail_item_attachment_file_box" app:layout_constraintStart_toStartOf="parent"
|
android:id="@+id/detail_item_attachment_file_box" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginStart="12dp" android:layout_marginEnd="12dp"
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginStart="12dp" android:layout_marginEnd="6dp"
|
||||||
android:visibility="visible" android:layout_marginTop="2dp"
|
android:visibility="visible" android:layout_marginTop="2dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:clickable="true" android:focusable="true" android:padding="4dp" android:paddingStart="0dp">
|
android:clickable="true" android:focusable="true" android:padding="4dp" android:paddingStart="0dp">
|
||||||
|
@ -193,5 +190,21 @@
|
||||||
android:id="@+id/detail_item_padding_bottom"
|
android:id="@+id/detail_item_padding_bottom"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_actions_wrapper" app:layout_constraintBottom_toBottomOf="parent"/>
|
app:layout_constraintTop_toBottomOf="@id/detail_item_actions_wrapper" app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:srcCompat="@drawable/ic_notification"
|
||||||
|
android:id="@+id/detail_item_icon"
|
||||||
|
android:visibility="visible"
|
||||||
|
android:maxHeight="40dp"
|
||||||
|
android:maxWidth="40dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="fitStart"
|
||||||
|
android:padding="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/detail_item_date_text"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/detail_item_message_text"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/detail_item_menu_button"
|
||||||
|
android:layout_marginEnd="6dp"/>
|
||||||
|
<androidx.constraintlayout.widget.Guideline android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/guideline2" app:layout_constraintGuide_begin="27dp" android:orientation="horizontal"/>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
|
@ -19,11 +19,11 @@
|
||||||
\n%2$s</string>
|
\n%2$s</string>
|
||||||
<string name="refresh_message_error_one">Tidak dapat memuat ulang langganan: %1$s</string>
|
<string name="refresh_message_error_one">Tidak dapat memuat ulang langganan: %1$s</string>
|
||||||
<string name="main_action_bar_title">Topik berlangganan</string>
|
<string name="main_action_bar_title">Topik berlangganan</string>
|
||||||
<string name="main_menu_notifications_enabled">Notifikasi nyala</string>
|
<string name="main_menu_notifications_enabled">Notifikasi menyala</string>
|
||||||
<string name="main_menu_notifications_disabled_forever">Notifikasi dibisukan</string>
|
<string name="main_menu_notifications_disabled_forever">Notifikasi dibisukan</string>
|
||||||
<string name="main_menu_notifications_disabled_until">Notifikasi dibisukan sampai %1$s</string>
|
<string name="main_menu_notifications_disabled_until">Notifikasi dibisukan sampai %1$s</string>
|
||||||
<string name="main_menu_settings_title">Pengaturan</string>
|
<string name="main_menu_settings_title">Pengaturan</string>
|
||||||
<string name="main_menu_report_bug_title">Laporkan sebuah kutu</string>
|
<string name="main_menu_report_bug_title">Laporkan sebuah bug</string>
|
||||||
<string name="main_menu_docs_title">Baca dokumentasi</string>
|
<string name="main_menu_docs_title">Baca dokumentasi</string>
|
||||||
<string name="main_menu_rate_title">Beri nilai aplikasi ⭐</string>
|
<string name="main_menu_rate_title">Beri nilai aplikasi ⭐</string>
|
||||||
<string name="main_action_mode_menu_unsubscribe">Batalkan langganan</string>
|
<string name="main_action_mode_menu_unsubscribe">Batalkan langganan</string>
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
<string name="main_how_to_intro">Tekan + untuk membuat atau berlangganan ke sebuah topik. Setelah itu Anda menerima notifikasi pada perangkat Anda saat mengirim pesan via PUT atau POST.</string>
|
<string name="main_how_to_intro">Tekan + untuk membuat atau berlangganan ke sebuah topik. Setelah itu Anda menerima notifikasi pada perangkat Anda saat mengirim pesan via PUT atau POST.</string>
|
||||||
<string name="main_how_to_link">Instruksi rinci tersedia di ntfy.sh, dan dalam dokumentasi.</string>
|
<string name="main_how_to_link">Instruksi rinci tersedia di ntfy.sh, dan dalam dokumentasi.</string>
|
||||||
<string name="main_unified_push_toast">Langganan ini dikelola oleh %1$s melalui UnifiedPush</string>
|
<string name="main_unified_push_toast">Langganan ini dikelola oleh %1$s melalui UnifiedPush</string>
|
||||||
<string name="main_banner_battery_text">Pengoptimalan baterai untuk aplikasi seharusnya mati supaya masalah pengiriman notifikasi dapat dihindari.</string>
|
<string name="main_banner_battery_text">Pengoptimalan baterai untuk aplikasi sebaiknya dimatikan supaya masalah pengiriman notifikasi dapat dihindari.</string>
|
||||||
<string name="main_banner_battery_button_remind_later">Tanya nanti</string>
|
<string name="main_banner_battery_button_remind_later">Tanya nanti</string>
|
||||||
<string name="main_banner_battery_button_dismiss">Abaikan</string>
|
<string name="main_banner_battery_button_dismiss">Abaikan</string>
|
||||||
<string name="main_banner_battery_button_fix_now">Perbaiki sekarang</string>
|
<string name="main_banner_battery_button_fix_now">Perbaiki sekarang</string>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
<string name="add_dialog_login_error_not_authorized">Login gagal. Pengguna %1$s tidak diizinkan.</string>
|
<string name="add_dialog_login_error_not_authorized">Login gagal. Pengguna %1$s tidak diizinkan.</string>
|
||||||
<string name="add_dialog_login_new_user">Pengguna baru</string>
|
<string name="add_dialog_login_new_user">Pengguna baru</string>
|
||||||
<string name="detail_no_notifications_text">Anda belum menerima notifikasi apa pun.</string>
|
<string name="detail_no_notifications_text">Anda belum menerima notifikasi apa pun.</string>
|
||||||
<string name="detail_how_to_intro">Untuk mengirimkan notifikasi ke topik ini, tinggal PUT atau POST ke URL topik.</string>
|
<string name="detail_how_to_intro">Untuk mengirimkan notifikasi ke topik ini, lakukan PUT atau POST ke URL topik.</string>
|
||||||
<string name="detail_how_to_example">Contoh (menggunakan curl):<br/><tt>$ curl -d \"Hai\" %1$s</tt></string>
|
<string name="detail_how_to_example">Contoh (menggunakan curl):<br/><tt>$ curl -d \"Hai\" %1$s</tt></string>
|
||||||
<string name="detail_how_to_link">Instruksi rinci tersedia di ntfy.sh, dan dalam dokumentasi.</string>
|
<string name="detail_how_to_link">Instruksi rinci tersedia di ntfy.sh, dan dalam dokumentasi.</string>
|
||||||
<string name="detail_clear_dialog_message">Hapus semua notifikasi di topik ini\?</string>
|
<string name="detail_clear_dialog_message">Hapus semua notifikasi di topik ini\?</string>
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
<string name="detail_test_message_error_unauthorized_anon">Tidak dapat mengirimkan pesan: Penerbitan anonim tidak diizinkan.</string>
|
<string name="detail_test_message_error_unauthorized_anon">Tidak dapat mengirimkan pesan: Penerbitan anonim tidak diizinkan.</string>
|
||||||
<string name="detail_test_message_error_too_large">Tidak dapat mengirimkan pesan: Lampiran terlalu besar.</string>
|
<string name="detail_test_message_error_too_large">Tidak dapat mengirimkan pesan: Lampiran terlalu besar.</string>
|
||||||
<string name="detail_copied_to_clipboard_message">Disalin ke papan klip</string>
|
<string name="detail_copied_to_clipboard_message">Disalin ke papan klip</string>
|
||||||
<string name="detail_instant_delivery_enabled">Pengiriman instan nyala</string>
|
<string name="detail_instant_delivery_enabled">Pengiriman instan menyala</string>
|
||||||
<string name="detail_instant_delivery_disabled">Pengiriman instan mati</string>
|
<string name="detail_instant_delivery_disabled">Pengiriman instan mati</string>
|
||||||
<string name="detail_item_tags">Tanda: %1$s</string>
|
<string name="detail_item_tags">Tanda: %1$s</string>
|
||||||
<string name="detail_item_snack_deleted">Notifikasi dihapus</string>
|
<string name="detail_item_snack_deleted">Notifikasi dihapus</string>
|
||||||
|
@ -165,8 +165,8 @@
|
||||||
<string name="settings_general_users_prefs_user_used_by_many">Digunakan oleh topik %1$s</string>
|
<string name="settings_general_users_prefs_user_used_by_many">Digunakan oleh topik %1$s</string>
|
||||||
<string name="settings_general_users_prefs_user_add_title">Tambahkan pengguna baru</string>
|
<string name="settings_general_users_prefs_user_add_title">Tambahkan pengguna baru</string>
|
||||||
<string name="settings_general_dark_mode_title">Mode gelap</string>
|
<string name="settings_general_dark_mode_title">Mode gelap</string>
|
||||||
<string name="settings_general_dark_mode_summary_light">Mode terang nyala</string>
|
<string name="settings_general_dark_mode_summary_light">Mode terang menyala</string>
|
||||||
<string name="settings_general_dark_mode_summary_dark">Mode gelap nyala. Apakah Anda seorang vampir\?</string>
|
<string name="settings_general_dark_mode_summary_dark">Mode gelap menyala. Apakah Anda seorang vampir\?</string>
|
||||||
<string name="settings_general_dark_mode_entry_system">Gunakan bawaan sistem</string>
|
<string name="settings_general_dark_mode_entry_system">Gunakan bawaan sistem</string>
|
||||||
<string name="settings_general_dark_mode_entry_light">Mode terang</string>
|
<string name="settings_general_dark_mode_entry_light">Mode terang</string>
|
||||||
<string name="settings_general_dark_mode_entry_dark">Mode gelap</string>
|
<string name="settings_general_dark_mode_entry_dark">Mode gelap</string>
|
||||||
|
@ -236,7 +236,7 @@
|
||||||
<string name="main_item_status_text_not_one">%1$d notifikasi</string>
|
<string name="main_item_status_text_not_one">%1$d notifikasi</string>
|
||||||
<string name="detail_test_message">Ini adalah notifikasi uji coba dari aplikasi Android ntfy. Ini memiliki tingkat prioritas %1$d. Jika Anda kirim yang lain, itu mungkin kelihatan berbeda.</string>
|
<string name="detail_test_message">Ini adalah notifikasi uji coba dari aplikasi Android ntfy. Ini memiliki tingkat prioritas %1$d. Jika Anda kirim yang lain, itu mungkin kelihatan berbeda.</string>
|
||||||
<string name="detail_test_message_error_unauthorized_user">Tidak dapat mengirimkan pesan: Pengguna \"%1$s\" tidak diizinkan.</string>
|
<string name="detail_test_message_error_unauthorized_user">Tidak dapat mengirimkan pesan: Pengguna \"%1$s\" tidak diizinkan.</string>
|
||||||
<string name="detail_menu_notifications_enabled">Notifikasi nyala</string>
|
<string name="detail_menu_notifications_enabled">Notifikasi menyala</string>
|
||||||
<string name="notification_popup_file_download_successful">%1$s
|
<string name="notification_popup_file_download_successful">%1$s
|
||||||
\nFile: %2$s, terunduh</string>
|
\nFile: %2$s, terunduh</string>
|
||||||
<string name="detail_item_saved_successfully">Disimpan sebagai \"%1$s\" dalam folder \"Downloads\"</string>
|
<string name="detail_item_saved_successfully">Disimpan sebagai \"%1$s\" dalam folder \"Downloads\"</string>
|
||||||
|
@ -289,7 +289,7 @@
|
||||||
<string name="settings_backup_restore_restore_failed">Pemulihan gagal: %1$s</string>
|
<string name="settings_backup_restore_restore_failed">Pemulihan gagal: %1$s</string>
|
||||||
<string name="settings_advanced_header">Tingkat lanjut</string>
|
<string name="settings_advanced_header">Tingkat lanjut</string>
|
||||||
<string name="settings_advanced_broadcast_summary_enabled">Aplikasi dapat menerima notifikasi yang datang sebagai siaran</string>
|
<string name="settings_advanced_broadcast_summary_enabled">Aplikasi dapat menerima notifikasi yang datang sebagai siaran</string>
|
||||||
<string name="settings_advanced_export_logs_summary">Salin catatan ke papn klip, atau unggah ke nopaste.net (dimiliki oleh penulis ntfy). Nama host dan topik dapat disensor, notifikasi tidak akan disensor.</string>
|
<string name="settings_advanced_export_logs_summary">Salin catatan ke papan klip, atau unggah ke nopaste.net (dimiliki oleh penulis ntfy). Nama host dan topik dapat disensor, notifikasi tidak akan disensor.</string>
|
||||||
<string name="settings_about_version_copied_to_clipboard_message">Disalin ke papan klip</string>
|
<string name="settings_about_version_copied_to_clipboard_message">Disalin ke papan klip</string>
|
||||||
<string name="user_dialog_password_hint_add">Kata Sandi</string>
|
<string name="user_dialog_password_hint_add">Kata Sandi</string>
|
||||||
<string name="user_dialog_button_delete">Hapus pengguna</string>
|
<string name="user_dialog_button_delete">Hapus pengguna</string>
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
<string name="main_banner_websocket_button_dismiss">Afwijzen</string>
|
<string name="main_banner_websocket_button_dismiss">Afwijzen</string>
|
||||||
<string name="user_dialog_button_delete">Gebruiker verwijderen</string>
|
<string name="user_dialog_button_delete">Gebruiker verwijderen</string>
|
||||||
<string name="user_dialog_button_cancel">Annuleren</string>
|
<string name="user_dialog_button_cancel">Annuleren</string>
|
||||||
<string name="settings_advanced_connection_protocol_entry_ws">WebSockets</string>
|
<string name="settings_advanced_connection_protocol_entry_ws">WebSocket</string>
|
||||||
<string name="settings_about_version_title">Versie</string>
|
<string name="settings_about_version_title">Versie</string>
|
||||||
<string name="settings_about_header">Over</string>
|
<string name="settings_about_header">Over</string>
|
||||||
<string name="settings_advanced_connection_protocol_title">Verbindingsprotocol</string>
|
<string name="settings_advanced_connection_protocol_title">Verbindingsprotocol</string>
|
||||||
|
@ -164,8 +164,8 @@
|
||||||
<string name="settings_advanced_clear_logs_title">Logs verwijderen</string>
|
<string name="settings_advanced_clear_logs_title">Logs verwijderen</string>
|
||||||
<string name="settings_advanced_clear_logs_deleted_toast">Logs verwijderd</string>
|
<string name="settings_advanced_clear_logs_deleted_toast">Logs verwijderd</string>
|
||||||
<string name="user_dialog_description_add">Je kunt hier een gebruiker toevoegen. Alle onderwerpen voor de opgegeven server zullen deze gebruiker gebruiken.</string>
|
<string name="user_dialog_description_add">Je kunt hier een gebruiker toevoegen. Alle onderwerpen voor de opgegeven server zullen deze gebruiker gebruiken.</string>
|
||||||
<string name="settings_advanced_connection_protocol_summary_ws">Gebruik WebSockets om verbinding te maken met de server. Dit wordt de standaard in juni 2022.</string>
|
<string name="settings_advanced_connection_protocol_summary_ws">Gebruik WebSockets om verbinding te maken met de server. Dit is de aangeraden methode, maar deze kan extra configuratie in uw proxy vereisen.</string>
|
||||||
<string name="settings_advanced_connection_protocol_summary_jsonhttp">Gebruik een JSON stream via HTTP om verbinding te maken met de server. Deze methode is verouderd en wordt in juni 2022 verwijderd.</string>
|
<string name="settings_advanced_connection_protocol_summary_jsonhttp">Gebruik een JSON stream via HTTP om verbinding te maken met de server. Deze methode is getest maar kan meer batterij verbruiken.</string>
|
||||||
<string name="settings_advanced_export_logs_entry_upload_scrubbed">Upload en kopieer link (gecensureerd)</string>
|
<string name="settings_advanced_export_logs_entry_upload_scrubbed">Upload en kopieer link (gecensureerd)</string>
|
||||||
<string name="settings_advanced_export_logs_scrub_dialog_text">Deze onderwerpen/hostnamen zijn vervangen met fruitnamen zodat je het log kunt delen zonder zorgen:
|
<string name="settings_advanced_export_logs_scrub_dialog_text">Deze onderwerpen/hostnamen zijn vervangen met fruitnamen zodat je het log kunt delen zonder zorgen:
|
||||||
\n
|
\n
|
||||||
|
@ -317,4 +317,14 @@
|
||||||
<string name="detail_settings_appearance_icon_set_summary">Stel een icoon in wat zal worden weergegeven in notificaties</string>
|
<string name="detail_settings_appearance_icon_set_summary">Stel een icoon in wat zal worden weergegeven in notificaties</string>
|
||||||
<string name="detail_settings_appearance_icon_remove_title">Abonnementen icoon (tap om te verwijderen)</string>
|
<string name="detail_settings_appearance_icon_remove_title">Abonnementen icoon (tap om te verwijderen)</string>
|
||||||
<string name="detail_settings_global_setting_suffix">Gebruikt globale instelling</string>
|
<string name="detail_settings_global_setting_suffix">Gebruikt globale instelling</string>
|
||||||
|
<string name="detail_settings_appearance_display_name_default_summary">%1$s (standaard)</string>
|
||||||
|
<string name="detail_settings_appearance_display_name_title">Schermnaam</string>
|
||||||
|
<string name="detail_settings_appearance_display_name_message">Zet een schermnaam voor dit abonnement. Laat het veld leeg om de standaard naam te kiezen (%1$s).</string>
|
||||||
|
<string name="detail_settings_about_header">Over</string>
|
||||||
|
<string name="detail_settings_about_topic_url_title">Onderwerp URL</string>
|
||||||
|
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Gekopieerd naar klembord</string>
|
||||||
|
<string name="add_dialog_base_urls_dropdown_choose">Kies service URL</string>
|
||||||
|
<string name="add_dialog_base_urls_dropdown_clear">Service URL verwijderen</string>
|
||||||
|
<string name="main_banner_websocket_button_enable_now">Nu inschakelen</string>
|
||||||
|
<string name="main_banner_websocket_text">WebSockets is de aangeraden manier om te verbinden met uw server en kan batterij verbruik verminderen. Het kan <a href="https://ntfy.sh/docs/config/#nginxapache2caddy"> extra configuratie in uw proxy</a> vereisen. Dit kan omgeschakeld worden in de instellingen.</string>
|
||||||
</resources>
|
</resources>
|
|
@ -51,7 +51,7 @@
|
||||||
<string name="settings_notifications_auto_delete_one_day">Через один день</string>
|
<string name="settings_notifications_auto_delete_one_day">Через один день</string>
|
||||||
<string name="settings_notifications_auto_delete_one_week">Через неделю</string>
|
<string name="settings_notifications_auto_delete_one_week">Через неделю</string>
|
||||||
<string name="settings_notifications_auto_delete_one_month">Через месяц</string>
|
<string name="settings_notifications_auto_delete_one_month">Через месяц</string>
|
||||||
<string name="settings_notifications_auto_delete_three_months">Чкрез три месяца</string>
|
<string name="settings_notifications_auto_delete_three_months">Через три месяца</string>
|
||||||
<string name="settings_general_header">Общие</string>
|
<string name="settings_general_header">Общие</string>
|
||||||
<string name="settings_general_default_base_url_title">Сервер по умолчанию</string>
|
<string name="settings_general_default_base_url_title">Сервер по умолчанию</string>
|
||||||
<string name="settings_general_default_base_url_default_summary">%1$s (по умолчанию)</string>
|
<string name="settings_general_default_base_url_default_summary">%1$s (по умолчанию)</string>
|
||||||
|
@ -194,7 +194,7 @@
|
||||||
<string name="notification_popup_file_downloading">Скачивается %1$s, %2$d%%
|
<string name="notification_popup_file_downloading">Скачивается %1$s, %2$d%%
|
||||||
\n%3$s</string>
|
\n%3$s</string>
|
||||||
<string name="notification_popup_file_download_successful">%1$s
|
<string name="notification_popup_file_download_successful">%1$s
|
||||||
\nФайл: %2$s, скачен</string>
|
\nФайл: %2$s, скачан</string>
|
||||||
<string name="notification_popup_file_download_failed">%1$s
|
<string name="notification_popup_file_download_failed">%1$s
|
||||||
\nФайл: %2$s, не удалось скачать</string>
|
\nФайл: %2$s, не удалось скачать</string>
|
||||||
<string name="settings_notifications_muted_until_title">Приостановить уведомления</string>
|
<string name="settings_notifications_muted_until_title">Приостановить уведомления</string>
|
||||||
|
@ -306,4 +306,5 @@
|
||||||
<string name="channel_subscriber_notification_instant_text_six">Подписан на шесть тем с мгновенной доставкой</string>
|
<string name="channel_subscriber_notification_instant_text_six">Подписан на шесть тем с мгновенной доставкой</string>
|
||||||
<string name="channel_subscriber_notification_noinstant_text_five">Подписан на пять тем</string>
|
<string name="channel_subscriber_notification_noinstant_text_five">Подписан на пять тем</string>
|
||||||
<string name="channel_subscriber_notification_noinstant_text_six">Подписан на шесть тем</string>
|
<string name="channel_subscriber_notification_noinstant_text_six">Подписан на шесть тем</string>
|
||||||
|
<string name="main_banner_websocket_button_enable_now">Включить сейчас</string>
|
||||||
</resources>
|
</resources>
|
|
@ -8,4 +8,323 @@
|
||||||
<string name="channel_subscriber_service_name">Абонементна Послуга</string>
|
<string name="channel_subscriber_service_name">Абонементна Послуга</string>
|
||||||
<string name="channel_subscriber_notification_title">Очікую вхідні сповіщення</string>
|
<string name="channel_subscriber_notification_title">Очікую вхідні сповіщення</string>
|
||||||
<string name="channel_subscriber_notification_instant_text">Підписався на теми миттєвої доставки</string>
|
<string name="channel_subscriber_notification_instant_text">Підписався на теми миттєвої доставки</string>
|
||||||
|
<string name="detail_settings_appearance_display_name_title">Відображуване ім\'я</string>
|
||||||
|
<string name="detail_settings_appearance_display_name_default_summary">%1$s (за умовчанням)</string>
|
||||||
|
<string name="detail_settings_appearance_display_name_message">Встановіть спеціальну відображувану назву для цієї підписки. Залиште поле порожнім для умовчання (%1$s).</string>
|
||||||
|
<string name="user_dialog_base_url_hint">URL служби</string>
|
||||||
|
<string name="add_dialog_use_another_server_description">Введіть URL-адреси служби нижче, щоб підписатися на теми з інших серверів.</string>
|
||||||
|
<string name="detail_item_cannot_open_url">Неможливо відкрити URL: %1$s</string>
|
||||||
|
<string name="detail_item_menu_cancel">Скасувати завантаження</string>
|
||||||
|
<string name="share_topic_title">Поділитися з</string>
|
||||||
|
<string name="settings_notifications_auto_delete_summary_one_month">Автоматичне видалення сповіщень через місяць</string>
|
||||||
|
<string name="settings_general_default_base_url_title">Сервер за замовчуванням</string>
|
||||||
|
<string name="notification_popup_user_action_failed">%1$s не вдалося: %2$s</string>
|
||||||
|
<string name="settings_notifications_header">Сповіщення</string>
|
||||||
|
<string name="settings_notifications_muted_until_forever">Сповіщення вимкнено до відновлення</string>
|
||||||
|
<string name="settings_notifications_muted_until_x">Сповіщення вимкнено до %1$s</string>
|
||||||
|
<string name="settings_general_dark_mode_title">Темний режим</string>
|
||||||
|
<string name="settings_general_dark_mode_summary_dark">Темний режим увімкнено. Ви вампір\?</string>
|
||||||
|
<string name="settings_general_dark_mode_entry_system">Використовувати систему за умовчанням</string>
|
||||||
|
<string name="settings_general_dark_mode_entry_light">Світловий режим</string>
|
||||||
|
<string name="settings_backup_restore_backup_failed">Помилка резервного копіювання: %1$s</string>
|
||||||
|
<string name="settings_backup_restore_restore_title">Відновити з файлу</string>
|
||||||
|
<string name="settings_advanced_header">Просунутий</string>
|
||||||
|
<string name="settings_advanced_export_logs_title">Копіювати/завантажувати журнали</string>
|
||||||
|
<string name="settings_notifications_channel_prefs_title">Налаштування каналу</string>
|
||||||
|
<string name="settings_notifications_channel_prefs_summary">Перевизначення режиму \"Не турбувати\" (DND), звуки тощо.</string>
|
||||||
|
<string name="settings_notifications_auto_download_summary_smaller_than_x">Автоматичне завантаження вкладень до %1$s</string>
|
||||||
|
<string name="settings_notifications_auto_download_never">Ніколи нічого не завантажуйте автоматично</string>
|
||||||
|
<string name="settings_notifications_auto_download_100k">Якщо менше 100 кБ</string>
|
||||||
|
<string name="settings_backup_restore_backup_summary">Експортуйте конфігурацію, сповіщення та користувачів</string>
|
||||||
|
<string name="settings_backup_restore_header">Резервне копіювання та відновлення</string>
|
||||||
|
<string name="settings_backup_restore_backup_title">Резервне копіювання в файл</string>
|
||||||
|
<string name="settings_backup_restore_backup_entry_everything">Все</string>
|
||||||
|
<string name="settings_backup_restore_backup_entry_everything_no_users">Все, крім користувачів</string>
|
||||||
|
<string name="settings_backup_restore_backup_entry_settings_only">Лише налаштування</string>
|
||||||
|
<string name="channel_subscriber_notification_instant_text_five">Підписався на п\'ять тем миттєвої доставки</string>
|
||||||
|
<string name="channel_subscriber_notification_instant_text_six">Підписався на шість тем миттєвої доставки</string>
|
||||||
|
<string name="channel_subscriber_notification_noinstant_text_five">Підписався на п\'ять тем</string>
|
||||||
|
<string name="channel_subscriber_notification_noinstant_text_six">Підписався на шість тем</string>
|
||||||
|
<string name="refresh_message_error">Не вдалося оновити %1$d підписок
|
||||||
|
\n
|
||||||
|
\n%2$s</string>
|
||||||
|
<string name="main_menu_settings_title">Налаштування</string>
|
||||||
|
<string name="main_menu_report_bug_title">Повідомити про помилку</string>
|
||||||
|
<string name="main_action_mode_menu_unsubscribe">Відписатися</string>
|
||||||
|
<string name="main_action_mode_delete_dialog_message">Скасувати підписку на вибрані теми та остаточно видалити всі сповіщення\?</string>
|
||||||
|
<string name="main_action_mode_delete_dialog_permanently_delete">Видалити назавжди</string>
|
||||||
|
<string name="main_action_mode_delete_dialog_cancel">Скасувати</string>
|
||||||
|
<string name="main_item_status_text_one">%1$d сповіщення</string>
|
||||||
|
<string name="main_item_status_text_not_one">%1$d сповіщень</string>
|
||||||
|
<string name="main_item_status_reconnecting">повторне підключення…</string>
|
||||||
|
<string name="main_item_status_unified_push">%1$s (UnifiedPush)</string>
|
||||||
|
<string name="main_item_date_yesterday">вчора</string>
|
||||||
|
<string name="main_add_button_description">Додати підписку</string>
|
||||||
|
<string name="main_no_subscriptions_text">Схоже, у вас ще немає жодної підписки.</string>
|
||||||
|
<string name="main_how_to_link">Детальні інструкції доступні на ntfy.sh і в документах.</string>
|
||||||
|
<string name="main_unified_push_toast">Цією підпискою керує %1$s через UnifiedPush</string>
|
||||||
|
<string name="main_banner_battery_text">Щоб уникнути проблем із доставляння сповіщень, оптимізацію акумулятора слід вимкнути.</string>
|
||||||
|
<string name="main_banner_battery_button_remind_later">Запитайте пізніше</string>
|
||||||
|
<string name="main_banner_battery_button_dismiss">Відхилити</string>
|
||||||
|
<string name="main_banner_battery_button_fix_now">Виправ зараз</string>
|
||||||
|
<string name="add_dialog_title">Підпишіться на тему</string>
|
||||||
|
<string name="add_dialog_description_below">Теми можуть не бути захищені паролем, тому виберіть назву, яку важко вгадати. Після підписки ви можете PUT/POST сповіщення.</string>
|
||||||
|
<string name="add_dialog_topic_name_hint">Назва теми, наприклад phils_alerts</string>
|
||||||
|
<string name="add_dialog_use_another_server">Використовуйте інший сервер</string>
|
||||||
|
<string name="add_dialog_instant_delivery">Миттєва доставка в режимі дрімання</string>
|
||||||
|
<string name="add_dialog_instant_delivery_description">Забезпечує миттєву доставку повідомлень, навіть якщо пристрій неактивний.</string>
|
||||||
|
<string name="add_dialog_foreground_description">Миттєва доставка завжди ввімкнена для хостів, відмінних від %1$s.</string>
|
||||||
|
<string name="add_dialog_button_cancel">Скасувати</string>
|
||||||
|
<string name="add_dialog_button_subscribe">Підпишіться</string>
|
||||||
|
<string name="add_dialog_button_back">Назад</string>
|
||||||
|
<string name="detail_clear_dialog_cancel">Скасувати</string>
|
||||||
|
<string name="detail_delete_dialog_message">Скасувати підписку на цю тему та видалити всі отримані сповіщення\?</string>
|
||||||
|
<string name="detail_delete_dialog_cancel">Скасувати</string>
|
||||||
|
<string name="detail_item_menu_save_file">Зберегти файл</string>
|
||||||
|
<string name="detail_item_menu_copy_url">Копіювати URL</string>
|
||||||
|
<string name="detail_item_download_info_deleted">видалено</string>
|
||||||
|
<string name="detail_menu_unsubscribe">Відписатися</string>
|
||||||
|
<string name="detail_action_mode_delete_dialog_message">Видалити вибрані сповіщення назавжди\?</string>
|
||||||
|
<string name="detail_action_mode_delete_dialog_cancel">Скасувати</string>
|
||||||
|
<string name="share_menu_send">Поділіться</string>
|
||||||
|
<string name="notification_popup_file_download_failed">%1$s
|
||||||
|
\nФайл: %2$s, не вдалося завантажити</string>
|
||||||
|
<string name="settings_general_users_prefs_user_used_by_one">Використовується темою %1$s</string>
|
||||||
|
<string name="detail_settings_global_setting_suffix">за допомогою глобальних налаштувань</string>
|
||||||
|
<string name="channel_subscriber_notification_noinstant_text">Підписався на теми</string>
|
||||||
|
<string name="channel_subscriber_notification_instant_text_one">Підписався на одну тему миттєвої доставки</string>
|
||||||
|
<string name="channel_subscriber_notification_instant_text_two">Підписався на дві теми моментальної доставки</string>
|
||||||
|
<string name="channel_subscriber_notification_noinstant_text_one">Підписався на одну тему</string>
|
||||||
|
<string name="channel_subscriber_notification_noinstant_text_more">Підписано на %1$d тем</string>
|
||||||
|
<string name="channel_subscriber_notification_instant_text_three">Підписався на три теми моментальної доставки</string>
|
||||||
|
<string name="channel_subscriber_notification_instant_text_four">Підписався на чотири теми миттєвої доставки</string>
|
||||||
|
<string name="channel_subscriber_notification_instant_text_more">Підписано на %1$d тем миттєвої доставки</string>
|
||||||
|
<string name="channel_subscriber_notification_noinstant_text_two">Підписався на дві теми</string>
|
||||||
|
<string name="refresh_message_no_results">Все в актуальному стані</string>
|
||||||
|
<string name="refresh_message_result">Отримано %1$d сповіщень</string>
|
||||||
|
<string name="add_dialog_login_password_hint">Пароль</string>
|
||||||
|
<string name="settings_notifications_auto_delete_never">Ніколи</string>
|
||||||
|
<string name="settings_advanced_broadcast_summary_disabled">Програми не можуть отримувати сповіщення як трансляції</string>
|
||||||
|
<string name="settings_advanced_record_logs_title">Журнали запису</string>
|
||||||
|
<string name="user_dialog_password_hint_add">Пароль</string>
|
||||||
|
<string name="user_dialog_password_hint_edit">Пароль (незмінний, якщо залишити порожнім)</string>
|
||||||
|
<string name="user_dialog_button_cancel">Скасувати</string>
|
||||||
|
<string name="main_menu_notifications_disabled_forever">Сповіщення вимкнено</string>
|
||||||
|
<string name="detail_menu_notifications_disabled_until">Сповіщення вимкнено до %1$s</string>
|
||||||
|
<string name="main_menu_notifications_disabled_until">Сповіщення вимкнено до %1$s</string>
|
||||||
|
<string name="channel_subscriber_notification_noinstant_text_three">Підписався на три теми</string>
|
||||||
|
<string name="channel_subscriber_notification_noinstant_text_four">Підписався на чотири теми</string>
|
||||||
|
<string name="main_action_bar_title">Підписані теми</string>
|
||||||
|
<string name="main_menu_docs_title">Читайте докуменацію</string>
|
||||||
|
<string name="notification_dialog_show_all">Показати всі сповіщення</string>
|
||||||
|
<string name="main_menu_notifications_enabled">Сповіщення ввімкнено</string>
|
||||||
|
<string name="settings_notifications_priority_default">за замовчуванням</string>
|
||||||
|
<string name="refresh_message_error_one">Не вдалося оновити підписку: %1$s</string>
|
||||||
|
<string name="main_menu_rate_title">Оцініть програму ⭐</string>
|
||||||
|
<string name="detail_test_message">Це тестове сповіщення від програми ntfy для Android. Він має рівень пріоритету %1$d. Якщо ви надішлете інший, він може виглядати інакше.</string>
|
||||||
|
<string name="main_how_to_intro">Натисніть +, щоб створити тему або підписатися на неї. Після цього ви отримуєте сповіщення на свій пристрій, коли надсилаєте повідомлення через PUT або POST.</string>
|
||||||
|
<string name="add_dialog_login_username_hint">Ім\'я користувача</string>
|
||||||
|
<string name="detail_copied_to_clipboard_message">Скопійовано в буфер обміну</string>
|
||||||
|
<string name="main_banner_websocket_text">Перехід на WebSockets є рекомендованим способом підключення до вашого сервера, який може подовжити час автономної роботи, але може вимагати <a href="https://ntfy.sh/docs/config/#nginxapache2caddy">додаткової конфігурації вашого проксі</a>. Це можна вимкнути в налаштуваннях.</string>
|
||||||
|
<string name="add_dialog_login_title">Необхідно ввійти</string>
|
||||||
|
<string name="add_dialog_login_new_user">Новий користувач</string>
|
||||||
|
<string name="detail_action_mode_menu_copy">Копія</string>
|
||||||
|
<string name="add_dialog_button_login">Авторизуватися</string>
|
||||||
|
<string name="add_dialog_error_connection_failed">Помилка підключення: %1$s</string>
|
||||||
|
<string name="add_dialog_login_description">Ця тема потребує авторизації. Будь ласка, введіть ім\'я користувача та пароль.</string>
|
||||||
|
<string name="add_dialog_login_error_not_authorized">Помилка логіну. Користувач %1$s не авторизований.</string>
|
||||||
|
<string name="detail_how_to_link">Детальні інструкції доступні на ntfy.sh і в документах.</string>
|
||||||
|
<string name="detail_clear_dialog_message">Видалити всі сповіщення в цій темі\?</string>
|
||||||
|
<string name="detail_item_cannot_save">Неможливо зберегти вкладення: %1$s</string>
|
||||||
|
<string name="detail_item_download_failed">Не вдалося завантажити вкладений файл: %1$s</string>
|
||||||
|
<string name="detail_how_to_example">Приклад (з використанням curl):<br/><tt>$ curl -d \"Hi\" %1$s</tt></string>
|
||||||
|
<string name="detail_item_download_info_not_downloaded_expires_x">не завантажено, закінчується %1$s</string>
|
||||||
|
<string name="detail_test_message_error_too_large">Не вдається надіслати повідомлення: вкладення завелике.</string>
|
||||||
|
<string name="detail_instant_delivery_enabled">Миттєва доставка включена</string>
|
||||||
|
<string name="detail_item_tags">Теги: %1$s</string>
|
||||||
|
<string name="detail_item_download_info_not_downloaded_expired">не завантажено, термін дії посилання закінчився</string>
|
||||||
|
<string name="detail_no_notifications_text">Ви ще не отримали сповіщень щодо цієї теми.</string>
|
||||||
|
<string name="detail_how_to_intro">Щоб надіслати сповіщення до цієї теми, просто PUT або POST за URL-адресою теми.</string>
|
||||||
|
<string name="detail_deep_link_subscribed_toast_message">Підписався на тему %1$s</string>
|
||||||
|
<string name="detail_item_cannot_open">Неможливо відкрити вкладення: %1$s</string>
|
||||||
|
<string name="detail_item_download_info_deleted_expires_x">видалено, посилання діє %1$s</string>
|
||||||
|
<string name="notification_dialog_tomorrow">До завтра</string>
|
||||||
|
<string name="detail_item_download_info_downloading_x_percent">%1$d%% завантажено</string>
|
||||||
|
<string name="detail_item_download_info_deleted_expired">видалено, термін дії посилання закінчився</string>
|
||||||
|
<string name="detail_action_mode_menu_delete">Видалити</string>
|
||||||
|
<string name="detail_action_mode_delete_dialog_permanently_delete">Видалити остаточно</string>
|
||||||
|
<string name="share_content_file_text">Вам надали доступ до файлу</string>
|
||||||
|
<string name="notification_dialog_30min">30 хвилин</string>
|
||||||
|
<string name="notification_popup_action_browse">Переглядати</string>
|
||||||
|
<string name="notification_popup_file">%1$s
|
||||||
|
\nФайл: %2$s</string>
|
||||||
|
<string name="share_content_file_error">Неможливо прочитати інформацію про файл: %1$s</string>
|
||||||
|
<string name="notification_dialog_8h">8 годин</string>
|
||||||
|
<string name="notification_dialog_forever">До відновлення</string>
|
||||||
|
<string name="settings_notifications_min_priority_summary_max">Показувати сповіщення, якщо пріоритет 5 (макс.)</string>
|
||||||
|
<string name="settings_notifications_min_priority_min">Будь-який пріоритет</string>
|
||||||
|
<string name="settings_notifications_min_priority_low">Низький пріоритет і вище</string>
|
||||||
|
<string name="notification_popup_file_downloading">Завантаження %1$s, %2$d%%
|
||||||
|
\n%3$s</string>
|
||||||
|
<string name="settings_advanced_broadcast_summary_enabled">Програми можуть отримувати вхідні сповіщення як трансляції</string>
|
||||||
|
<string name="settings_notifications_muted_until_title">Вимкнути сповіщення</string>
|
||||||
|
<string name="settings_general_users_prefs_user_used_by_many">Використовується темами %1$s</string>
|
||||||
|
<string name="settings_notifications_auto_delete_summary_one_week">Автоматичне видалення сповіщень через тиждень</string>
|
||||||
|
<string name="settings_notifications_priority_low">низький</string>
|
||||||
|
<string name="settings_notifications_auto_download_10m">Якщо менше 10 Мб</string>
|
||||||
|
<string name="settings_notifications_auto_delete_one_day">Через один день</string>
|
||||||
|
<string name="settings_notifications_auto_download_50m">Якщо менше 50 Мб</string>
|
||||||
|
<string name="settings_notifications_auto_delete_title">Видалити сповіщення</string>
|
||||||
|
<string name="settings_notifications_auto_delete_summary_three_days">Автоматичне видалення сповіщень через 3 дні</string>
|
||||||
|
<string name="settings_general_users_title">Керувати користувачами</string>
|
||||||
|
<string name="settings_general_dark_mode_summary_system">Використання системи за замовчуванням</string>
|
||||||
|
<string name="settings_general_dark_mode_summary_light">Світловий режим включений</string>
|
||||||
|
<string name="settings_advanced_broadcast_title">Трансляція повідомлень</string>
|
||||||
|
<string name="settings_general_users_summary">Додавання/видалення користувачів для захищених тем</string>
|
||||||
|
<string name="settings_general_users_prefs_user_add_title">Додати нового користувача</string>
|
||||||
|
<string name="settings_general_users_prefs_user_add_summary">Створіть нового користувача для нового сервера</string>
|
||||||
|
<string name="settings_general_dark_mode_entry_dark">Темний режим</string>
|
||||||
|
<string name="settings_backup_restore_restore_failed">Не вдалося відновити: %1$s</string>
|
||||||
|
<string name="settings_backup_restore_backup_successful">Резервну копію створено</string>
|
||||||
|
<string name="settings_backup_restore_restore_summary">Імпортуйте конфігурацію, сповіщення та користувачів</string>
|
||||||
|
<string name="settings_backup_restore_restore_successful">Відновлено успішно</string>
|
||||||
|
<string name="settings_advanced_export_logs_summary">Скопіюйте журнали в буфер обміну або завантажте на nopaste.net (належить автору ntfy). Імена хостів і теми можуть бути піддані цензурі, сповіщення – ніколи.</string>
|
||||||
|
<string name="settings_advanced_export_logs_entry_copy_scrubbed">Копіювати в буфер обміну (цензуровано)</string>
|
||||||
|
<string name="settings_advanced_export_logs_entry_copy_original">Копіювати в буфер обміну</string>
|
||||||
|
<string name="settings_advanced_export_logs_uploading">Завантаження журналу…</string>
|
||||||
|
<string name="settings_advanced_export_logs_scrub_dialog_text">Ці теми/імена хостів було замінено назвами фруктів, тож ви можете ділитися журналом без хвилювань:
|
||||||
|
\n
|
||||||
|
\n%1$s
|
||||||
|
\n
|
||||||
|
\nПаролі очищаються, але не відображаються тут.</string>
|
||||||
|
<string name="settings_advanced_export_logs_entry_upload_original">Завантажте та скопіюйте посилання</string>
|
||||||
|
<string name="settings_advanced_export_logs_entry_upload_scrubbed">Завантажити та скопіювати посилання (цензуровано)</string>
|
||||||
|
<string name="settings_advanced_export_logs_copied_logs">Журнали скопійовано в буфер обміну</string>
|
||||||
|
<string name="settings_advanced_clear_logs_summary">Видаліть раніше записані журнали та почніть спочатку</string>
|
||||||
|
<string name="settings_advanced_connection_protocol_summary_ws">Використовуйте WebSockets для підключення до сервера. Це рекомендований метод, але може знадобитися додаткова конфігурація вашого проксі.</string>
|
||||||
|
<string name="user_dialog_button_save">Зберегти</string>
|
||||||
|
<string name="detail_settings_appearance_icon_set_title">Значок підписки</string>
|
||||||
|
<string name="detail_settings_notifications_instant_summary_on">Сповіщення надходять миттєво. Потрібна служба переднього плану та споживає більше акумулятора.</string>
|
||||||
|
<string name="detail_settings_notifications_instant_summary_off">Сповіщення доставляються за допомогою Firebase. Доставка може бути відкладена, але споживає менше акумулятора.</string>
|
||||||
|
<string name="detail_settings_about_topic_url_title">URL теми</string>
|
||||||
|
<string name="user_dialog_username_hint">Ім\'я користувача</string>
|
||||||
|
<string name="detail_settings_about_header">Про</string>
|
||||||
|
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Скопійовано в буфер обміну</string>
|
||||||
|
<string name="user_dialog_description_edit">Ви можете змінити ім\'я користувача/пароль для вибраного користувача або видалити його.</string>
|
||||||
|
<string name="user_dialog_button_add">Додати користувача</string>
|
||||||
|
<string name="user_dialog_description_add">Ви можете додати користувача тут. Усі теми для даного сервера використовуватимуть цього користувача.</string>
|
||||||
|
<string name="user_dialog_button_delete">Видалити користувача</string>
|
||||||
|
<string name="add_dialog_base_urls_dropdown_choose">Виберіть URL-адресу служби</string>
|
||||||
|
<string name="add_dialog_base_urls_dropdown_clear">Очистити URL-адресу служби</string>
|
||||||
|
<string name="detail_clear_dialog_permanently_delete">Видалити остаточно</string>
|
||||||
|
<string name="detail_delete_dialog_permanently_delete">Видалити остаточно</string>
|
||||||
|
<string name="detail_test_title">Тест: Ви можете встановити назву, якщо хочете.</string>
|
||||||
|
<string name="detail_test_message_error">Неможливо надіслати повідомлення: %1$s</string>
|
||||||
|
<string name="detail_test_message_error_unauthorized_anon">Неможливо надіслати повідомлення: анонімна публікація заборонена.</string>
|
||||||
|
<string name="detail_test_message_error_unauthorized_user">Неможливо надіслати повідомлення: користувач \"%1$s\" не авторизований.</string>
|
||||||
|
<string name="detail_instant_delivery_disabled">Миттєва доставка вимкнена</string>
|
||||||
|
<string name="detail_item_snack_deleted">Сповіщення видалено</string>
|
||||||
|
<string name="detail_item_snack_undo">Скасувати</string>
|
||||||
|
<string name="detail_item_menu_open">Відкрити файл</string>
|
||||||
|
<string name="detail_item_menu_delete">Видалити файл</string>
|
||||||
|
<string name="detail_item_menu_download">Завантажити файл</string>
|
||||||
|
<string name="detail_item_menu_copy_url_copied">URL-адресу скопійовано в буфер обміну</string>
|
||||||
|
<string name="detail_item_menu_copy_contents">Копіювати сповіщення</string>
|
||||||
|
<string name="detail_item_menu_copy_contents_copied">Сповіщення скопійовано в буфер обміну</string>
|
||||||
|
<string name="detail_item_saved_successfully">Збережено як \"%1$s\" у папці \"Завантаження\"</string>
|
||||||
|
<string name="detail_item_cannot_download">Не вдається відкрити або завантажити вкладений файл. Термін дії посилання закінчився, і локальний файл не знайдено.</string>
|
||||||
|
<string name="detail_item_cannot_open_not_found">Неможливо відкрити вкладення: файл, можливо, видалено, або жодна встановлена програма не може відкрити файл.</string>
|
||||||
|
<string name="detail_item_cannot_delete">Неможливо видалити вкладення: %1$s</string>
|
||||||
|
<string name="detail_item_download_info_not_downloaded">не завантажено</string>
|
||||||
|
<string name="detail_item_download_info_download_failed">не вдалося завантажити</string>
|
||||||
|
<string name="detail_item_download_info_download_failed_expired">не вдалося завантажити, термін дії посилання закінчився</string>
|
||||||
|
<string name="detail_item_download_info_download_failed_expires_x">не вдалося завантажити, термін дії посилання закінчився %1$s</string>
|
||||||
|
<string name="detail_menu_notifications_enabled">Сповіщення ввімкнено</string>
|
||||||
|
<string name="detail_menu_notifications_disabled_forever">Сповіщення вимкнено</string>
|
||||||
|
<string name="detail_menu_enable_instant">Увімкнути миттєву доставку</string>
|
||||||
|
<string name="detail_menu_disable_instant">Вимкніть миттєву доставку</string>
|
||||||
|
<string name="detail_menu_test">Надіслати тестове сповіщення</string>
|
||||||
|
<string name="detail_menu_copy_url">Копіювати адресу теми</string>
|
||||||
|
<string name="detail_menu_clear">Очистити всі сповіщення</string>
|
||||||
|
<string name="detail_menu_settings">Налаштування підписки</string>
|
||||||
|
<string name="detail_settings_title">Налаштування підписки</string>
|
||||||
|
<string name="share_title">Поділіться</string>
|
||||||
|
<string name="share_content_title">Попередній перегляд повідомлення</string>
|
||||||
|
<string name="share_content_text_hint">Додайте вміст, щоб поділитися тут</string>
|
||||||
|
<string name="share_content_image_text">З вами поділилися зображенням</string>
|
||||||
|
<string name="share_content_image_error">Не вдається прочитати зображення: %1$s</string>
|
||||||
|
<string name="share_suggested_topics">Пропоновані теми</string>
|
||||||
|
<string name="share_successful">Повідомлення опубліковано</string>
|
||||||
|
<string name="notification_dialog_title">Вимкнути сповіщення</string>
|
||||||
|
<string name="notification_dialog_cancel">Скасувати</string>
|
||||||
|
<string name="notification_dialog_save">Зберегти</string>
|
||||||
|
<string name="notification_dialog_enabled_toast_message">Сповіщення відновлено</string>
|
||||||
|
<string name="notification_dialog_muted_forever_toast_message">Сповіщення вимкнено</string>
|
||||||
|
<string name="notification_dialog_muted_until_toast_message">Сповіщення вимкнено до %1$s</string>
|
||||||
|
<string name="notification_dialog_1h">1 година</string>
|
||||||
|
<string name="notification_dialog_2h">2 години</string>
|
||||||
|
<string name="notification_popup_action_open">Відчинено</string>
|
||||||
|
<string name="notification_popup_action_download">Завантажити</string>
|
||||||
|
<string name="notification_popup_action_cancel">Скасувати</string>
|
||||||
|
<string name="notification_popup_file_download_successful">%1$s
|
||||||
|
\nФайл: %2$s, завантажено</string>
|
||||||
|
<string name="settings_title">Налаштування</string>
|
||||||
|
<string name="settings_notifications_muted_until_show_all">Показано всі сповіщення</string>
|
||||||
|
<string name="settings_notifications_min_priority_title">Мінімальний пріоритет</string>
|
||||||
|
<string name="settings_notifications_min_priority_summary_any">Показано всі сповіщення</string>
|
||||||
|
<string name="settings_notifications_min_priority_summary_x_or_higher">Показувати сповіщення, якщо пріоритет %1$d (%2$s) або вище</string>
|
||||||
|
<string name="settings_notifications_min_priority_default">Пріоритет за замовчуванням і вище</string>
|
||||||
|
<string name="settings_notifications_min_priority_high">Високий пріоритет і вище</string>
|
||||||
|
<string name="settings_notifications_min_priority_max">Тільки максимальний пріоритет</string>
|
||||||
|
<string name="settings_notifications_priority_min">Мін</string>
|
||||||
|
<string name="settings_notifications_priority_high">високій</string>
|
||||||
|
<string name="settings_notifications_priority_max">макс</string>
|
||||||
|
<string name="settings_notifications_auto_download_title">Завантажити вкладення</string>
|
||||||
|
<string name="settings_notifications_auto_download_summary_always">Автоматичне завантаження всіх вкладень</string>
|
||||||
|
<string name="settings_notifications_auto_download_summary_never">Ніколи не завантажуйте вкладені файли автоматично</string>
|
||||||
|
<string name="settings_notifications_auto_download_always">Автоматичне завантаження всього</string>
|
||||||
|
<string name="settings_notifications_auto_download_500k">Якщо менше 500 кБ</string>
|
||||||
|
<string name="settings_notifications_auto_download_1m">Якщо менше 1 Мб</string>
|
||||||
|
<string name="settings_notifications_auto_download_5m">Якщо менше 5 Мб</string>
|
||||||
|
<string name="settings_notifications_auto_delete_summary_never">Ніколи автоматично не видаляйте сповіщення</string>
|
||||||
|
<string name="settings_notifications_auto_delete_summary_one_day">Автоматичне видалення сповіщень через один день</string>
|
||||||
|
<string name="settings_notifications_auto_delete_summary_three_months">Автоматичне видалення сповіщень через 3 місяці</string>
|
||||||
|
<string name="settings_notifications_auto_delete_three_days">Через 3 дні</string>
|
||||||
|
<string name="settings_notifications_auto_delete_one_week">Через тиждень</string>
|
||||||
|
<string name="settings_notifications_auto_delete_one_month">Через місяць</string>
|
||||||
|
<string name="settings_notifications_auto_delete_three_months">Через 3 міс</string>
|
||||||
|
<string name="user_dialog_title_add">Додати користувача</string>
|
||||||
|
<string name="settings_general_header">Загальний</string>
|
||||||
|
<string name="settings_general_default_base_url_message">Введіть кореневу URL-адресу свого сервера, щоб використовувати свій власний сервер як стандартний під час підписки на нові теми та/або спільного доступу до тем.</string>
|
||||||
|
<string name="settings_general_default_base_url_default_summary">%1$s (за умовчанням)</string>
|
||||||
|
<string name="settings_general_users_prefs_title">Користувачі</string>
|
||||||
|
<string name="settings_general_users_prefs_user_not_used">Не використовується жодною темою</string>
|
||||||
|
<string name="settings_general_users_prefs_user_add">Додайте користувачів</string>
|
||||||
|
<string name="settings_advanced_record_logs_summary_enabled">Реєстрація (до 1000 записів) на пристрої…</string>
|
||||||
|
<string name="settings_advanced_record_logs_summary_disabled">Увімкніть ведення журналів, щоб пізніше ви могли поділитися журналами для діагностики проблем.</string>
|
||||||
|
<string name="settings_advanced_export_logs_copied_url">Журнали завантажено та URL-адресу скопійовано</string>
|
||||||
|
<string name="settings_advanced_export_logs_error_uploading">Не вдалося завантажити журнали: %1$s</string>
|
||||||
|
<string name="settings_advanced_export_logs_scrub_dialog_empty">Не було відредаговано жодної теми/імена хостів. Може у вас немає підписки\?</string>
|
||||||
|
<string name="settings_advanced_export_logs_scrub_dialog_button_ok">ОК</string>
|
||||||
|
<string name="settings_advanced_clear_logs_title">Очистити журнали</string>
|
||||||
|
<string name="settings_advanced_clear_logs_deleted_toast">Журнали видалено</string>
|
||||||
|
<string name="settings_advanced_connection_protocol_title">Протокол підключення</string>
|
||||||
|
<string name="settings_advanced_connection_protocol_entry_jsonhttp">Потік JSON через HTTP</string>
|
||||||
|
<string name="settings_advanced_connection_protocol_entry_ws">WebSockets</string>
|
||||||
|
<string name="settings_about_header">Про</string>
|
||||||
|
<string name="settings_about_version_title">Версія</string>
|
||||||
|
<string name="settings_about_version_format">ntfy %1$s (%2$s)</string>
|
||||||
|
<string name="settings_about_version_copied_to_clipboard_message">Скопійовано в буфер обміну</string>
|
||||||
|
<string name="detail_settings_notifications_instant_title">Миттєва доставка</string>
|
||||||
|
<string name="detail_settings_appearance_header">Зовнішній вигляд</string>
|
||||||
|
<string name="detail_settings_appearance_icon_set_summary">Установіть піктограму, яка відображатиметься в сповіщеннях</string>
|
||||||
|
<string name="detail_settings_appearance_icon_remove_title">Значок підписки (натисніть, щоб видалити)</string>
|
||||||
|
<string name="detail_settings_appearance_icon_remove_summary">Значок, який відображається в сповіщеннях для цієї теми</string>
|
||||||
|
<string name="detail_settings_appearance_icon_error_saving">Не вдалося зберегти значок: %1$s</string>
|
||||||
|
<string name="detail_settings_global_setting_title">Використовуйте глобальні налаштування</string>
|
||||||
|
<string name="user_dialog_title_edit">Редагувати користувача</string>
|
||||||
|
<string name="main_banner_websocket_button_remind_later">Запитайте пізніше</string>
|
||||||
|
<string name="main_banner_websocket_button_dismiss">Відхилити</string>
|
||||||
|
<string name="main_banner_websocket_button_enable_now">Увімкнути зараз</string>
|
||||||
|
<string name="settings_advanced_connection_protocol_summary_jsonhttp">Використовуйте потік JSON через HTTP для підключення до сервера. Цей метод перевірено в боях, але може споживати більше заряду батареї.</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,17 +1,16 @@
|
||||||
package io.heckel.ntfy.firebase
|
package io.heckel.ntfy.firebase
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Base64
|
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.Attachment
|
import io.heckel.ntfy.db.Attachment
|
||||||
|
import io.heckel.ntfy.db.Icon
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
import io.heckel.ntfy.msg.NotificationParser
|
import io.heckel.ntfy.msg.NotificationParser
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
|
@ -90,6 +89,7 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
val priority = data["priority"]?.toIntOrNull()
|
val priority = data["priority"]?.toIntOrNull()
|
||||||
val tags = data["tags"]
|
val tags = data["tags"]
|
||||||
val click = data["click"]
|
val click = data["click"]
|
||||||
|
val iconUrl = data["icon"]
|
||||||
val actions = data["actions"] // JSON array as string, sigh ...
|
val actions = data["actions"] // JSON array as string, sigh ...
|
||||||
val encoding = data["encoding"]
|
val encoding = data["encoding"]
|
||||||
val attachmentName = data["attachment_name"] ?: "attachment.bin"
|
val attachmentName = data["attachment_name"] ?: "attachment.bin"
|
||||||
|
@ -124,6 +124,7 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
url = attachmentUrl,
|
url = attachmentUrl,
|
||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
|
val icon: Icon? = iconUrl?.let { Icon(url = it) }
|
||||||
val notification = Notification(
|
val notification = Notification(
|
||||||
id = id,
|
id = id,
|
||||||
subscriptionId = subscription.id,
|
subscriptionId = subscription.id,
|
||||||
|
@ -134,6 +135,7 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
priority = toPriority(priority),
|
priority = toPriority(priority),
|
||||||
tags = tags ?: "",
|
tags = tags ?: "",
|
||||||
click = click ?: "",
|
click = click ?: "",
|
||||||
|
icon = icon,
|
||||||
actions = parser.parseActions(actions),
|
actions = parser.parseActions(actions),
|
||||||
attachment = attachment,
|
attachment = attachment,
|
||||||
notificationId = Random.nextInt(),
|
notificationId = Random.nextInt(),
|
||||||
|
|
|
@ -4,14 +4,17 @@ Features:
|
||||||
* Polling is now done with since=<id> API, which makes deduping easier (#165)
|
* Polling is now done with since=<id> API, which makes deduping easier (#165)
|
||||||
* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
|
* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
|
||||||
* Move action buttons in notification cards (#236, thanks to @wunter8)
|
* Move action buttons in notification cards (#236, thanks to @wunter8)
|
||||||
|
* Icons can be set for each individual notification (#126, thanks to @wunter8)
|
||||||
|
|
||||||
Bugs:
|
Bugs:
|
||||||
* Long-click selecting of notifications doesn't scoll to the top anymore (#235, thanks to @wunter8)
|
* Long-click selecting of notifications doesn't scoll to the top anymore (#235, thanks to @wunter8)
|
||||||
* Add attachment and click URL extras to MESSAGE_RECEIVED broadcast (#329, thanks to @wunter8)
|
* Add attachment and click URL extras to MESSAGE_RECEIVED broadcast (#329, thanks to @wunter8)
|
||||||
* Accessibility: Clear/choose service URL button in base URL dropdown now has a label (#292, thanks to @mhameed for reporting)
|
* Accessibility: Clear/choose service URL button in base URL dropdown now has a label (#292, thanks to @mhameed for reporting)
|
||||||
|
* Do not crash app if preview image too large (no ticket)
|
||||||
|
|
||||||
Additional translations:
|
Additional translations:
|
||||||
* Italian (thanks to @Genio2003)
|
* Italian (thanks to @Genio2003)
|
||||||
* Dutch (thanks to @SchoNie)
|
* Dutch (thanks to @SchoNie)
|
||||||
|
* Ukranian (thanks to @v.kopitsa)
|
||||||
|
|
||||||
Thank you to @wunter8 for proactively picking up some Android tickets, and fixing them! You rock!
|
Thank you to @wunter8 for proactively picking up some Android tickets, and fixing them! You rock!
|
||||||
|
|
17
fastlane/metadata/android/uk/full_description.txt
Normal file
17
fastlane/metadata/android/uk/full_description.txt
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
Надсилайте сповіщення на свій телефон із будь-якого сценарію Bash або PowerShell або з власної програми за допомогою запитів PUT/POST, напр. через curl у Linux або Invoke-WebRequest.
|
||||||
|
|
||||||
|
ntfy — це Android-клієнт для https://ntfy.sh, безкоштовного HTTP-сервісу pub-sub з відкритим кодом. Ви можете підписатися на теми в цій програмі, а потім публікувати повідомлення через простий HTTP API.
|
||||||
|
|
||||||
|
Використання:
|
||||||
|
* Повідомте себе, коли довготривалий процес завершено
|
||||||
|
* Пропонуйте запит телефону, якщо не вдалося створити резервну копію
|
||||||
|
* Сповіщення, коли хтось входить на ваш сервер
|
||||||
|
|
||||||
|
приклад:
|
||||||
|
|
||||||
|
$ curl -d "Ваше резервне копіювання виконано" ntfy.sh/mytopic
|
||||||
|
|
||||||
|
Додаткові приклади та інструкції з використання можна знайти тут:
|
||||||
|
* Веб-сайт: https://ntfy.sh
|
||||||
|
* GitHub (сервер): https://github.com/binwiederhier/ntfy
|
||||||
|
* GitHub (програма для Android): https://github.com/binwiederhier/ntfy-android
|
1
fastlane/metadata/android/uk/short_description.txt
Normal file
1
fastlane/metadata/android/uk/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Надсилайте сповіщення на свій телефон за допомогою запитів PUT/POST
|
1
fastlane/metadata/android/uk/title.txt
Normal file
1
fastlane/metadata/android/uk/title.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ntfy - PUT/POST на ваш телефон
|
Loading…
Reference in a new issue