Merge branch 'main' into custom_notification_channels

This commit is contained in:
Philipp Heckel 2022-12-06 20:45:12 -05:00
commit 6f0bf4d112
41 changed files with 461 additions and 138 deletions

View file

@ -14,8 +14,8 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 29 versionCode 32
versionName "1.15.0" versionName "1.16.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -44,10 +44,12 @@ android {
play { play {
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true' buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
} }
fdroid { fdroid {
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false' buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
} }
} }
@ -64,12 +66,29 @@ android {
} }
} }
// Disables GoogleServices tasks for F-Droid variant
android.applicationVariants.all { variant -> android.applicationVariants.all { variant ->
def shouldProcessGoogleServices = variant.flavorName == "play" def shouldProcessGoogleServices = variant.flavorName == "play"
def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices") def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices")
googleTask.enabled = shouldProcessGoogleServices googleTask.enabled = shouldProcessGoogleServices
} }
// Strips out REQUEST_INSTALL_PACKAGES permission for Google Play variant
android.applicationVariants.all { variant ->
def shouldStripInstallPermission = variant.flavorName == "play"
if (shouldStripInstallPermission) {
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getMultiApkManifestOutputDirectory().get().asFile
def manifestOutFile = file("$outputDir/AndroidManifest.xml")
def newFileContents = manifestOutFile.collect { s -> s.contains("android.permission.REQUEST_INSTALL_PACKAGES") ? "" : s }.join("\n")
manifestOutFile.write(newFileContents, 'UTF-8')
}
}
}
}
dependencies { dependencies {
// AndroidX, The Basics // AndroidX, The Basics
implementation "androidx.appcompat:appcompat:1.5.1" implementation "androidx.appcompat:appcompat:1.5.1"

View file

@ -1,5 +1,6 @@
package io.heckel.ntfy.firebase package io.heckel.ntfy.firebase
@Suppress("UNUSED_PARAMETER")
class FirebaseMessenger { class FirebaseMessenger {
fun subscribe(topic: String) { fun subscribe(topic: String) {
// Dummy to keep F-Droid flavor happy // Dummy to keep F-Droid flavor happy

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.heckel.ntfy"> package="io.heckel.ntfy">
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service -->
@ -8,10 +9,17 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <!-- To restart service on reboot --> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <!-- To restart service on reboot -->
<uses-permission android:name="android.permission.VIBRATE"/> <!-- Incoming notifications should be able to vibrate the phone --> <uses-permission android:name="android.permission.VIBRATE"/> <!-- Incoming notifications should be able to vibrate the phone -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- Only required on SDK <= 28 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- Only required on SDK <= 28 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <!-- To install packages downloaded through ntfy; craazyy! -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- To reschedule the websocket retry --> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- To reschedule the websocket retry -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- As of Android 13, we need to ask for permission to post notifications --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- As of Android 13, we need to ask for permission to post notifications -->
<!--
Permission REQUEST_INSTALL_PACKAGES (F-Droid only!):
- Permission is used to install .apk files that were received as attachments
- Google rejected the permission for ntfy, so this permission is STRIPPED OUT by the build process
for the Google Play variant of the app.
-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application <application
android:name=".app.Application" android:name=".app.Application"
android:allowBackup="true" android:allowBackup="true"

View file

@ -1,7 +1,6 @@
package io.heckel.ntfy.app package io.heckel.ntfy.app
import android.app.Application import android.app.Application
import android.content.Context
import io.heckel.ntfy.db.Database import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log

View file

@ -89,7 +89,7 @@ class Backuper(val context: Context) {
private suspend fun applySubscriptions(subscriptions: List<Subscription>?) { private suspend fun applySubscriptions(subscriptions: List<Subscription>?) {
if (subscriptions == null) { if (subscriptions == null) {
return; return
} }
val appBaseUrl = context.getString(R.string.app_base_url) val appBaseUrl = context.getString(R.string.app_base_url)
subscriptions.forEach { s -> subscriptions.forEach { s ->
@ -120,7 +120,7 @@ class Backuper(val context: Context) {
private suspend fun applyNotifications(notifications: List<Notification>?) { private suspend fun applyNotifications(notifications: List<Notification>?) {
if (notifications == null) { if (notifications == null) {
return; return
} }
notifications.forEach { n -> notifications.forEach { n ->
try { try {
@ -189,7 +189,7 @@ class Backuper(val context: Context) {
private suspend fun applyUsers(users: List<User>?) { private suspend fun applyUsers(users: List<User>?) {
if (users == null) { if (users == null) {
return; return
} }
users.forEach { u -> users.forEach { u ->
try { try {

View file

@ -95,7 +95,7 @@ class ApiService {
throw Exception("Unexpected response ${response.code} when polling topic $url") throw Exception("Unexpected response ${response.code} when polling topic $url")
} }
val body = response.body?.string()?.trim() val body = response.body?.string()?.trim()
if (body == null || body.isEmpty()) return emptyList() if (body.isNullOrEmpty()) return emptyList()
val notifications = body.lines().mapNotNull { line -> val notifications = body.lines().mapNotNull { line ->
parser.parse(line, subscriptionId = subscriptionId, notificationId = 0) // No notification when we poll parser.parse(line, subscriptionId = subscriptionId, notificationId = 0) // No notification when we poll
} }
@ -166,7 +166,7 @@ class ApiService {
} }
class UnauthorizedException(val user: User?) : Exception() class UnauthorizedException(val user: User?) : Exception()
class EntityTooLargeException() : Exception() class EntityTooLargeException : Exception()
companion object { companion object {
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})" val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"

View file

@ -11,7 +11,7 @@ import io.heckel.ntfy.util.Log
* Download attachment in the background via WorkManager * Download attachment in the background via WorkManager
* *
* The indirection via WorkManager is required since this code may be executed * The indirection via WorkManager is required since this code may be executed
* in a doze state and Internet may not be available. It's also best practice apparently. * in a doze state and Internet may not be available. It's also best practice, apparently.
*/ */
object DownloadManager { object DownloadManager {
private const val TAG = "NtfyDownloadManager" private const val TAG = "NtfyDownloadManager"

View file

@ -206,6 +206,9 @@ class NotificationService(val context: Context) {
} }
private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) { private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) {
if (!canOpenAttachment(notification.attachment)) {
return
}
if (notification.attachment?.contentUri != null) { if (notification.attachment?.contentUri != null) {
val contentUri = Uri.parse(notification.attachment.contentUri) val contentUri = Uri.parse(notification.attachment.contentUri)
val intent = Intent(Intent.ACTION_VIEW, contentUri).apply { val intent = Intent(Intent.ACTION_VIEW, contentUri).apply {

View file

@ -200,7 +200,7 @@ class SubscriberService : Service() {
// retrieve old messages. This is important, so we don't download attachments from old messages. // retrieve old messages. This is important, so we don't download attachments from old messages.
val since = sinceByBaseUrl[connectionId.baseUrl] ?: "none" val since = sinceByBaseUrl[connectionId.baseUrl] ?: "none"
val serviceActive = { -> isServiceStarted } val serviceActive = { isServiceStarted }
val user = repository.getUser(connectionId.baseUrl) val user = repository.getUser(connectionId.baseUrl)
val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) { val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) {
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
@ -310,11 +310,11 @@ class SubscriberService : Service() {
override fun onTaskRemoved(rootIntent: Intent) { override fun onTaskRemoved(rootIntent: Intent) {
val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also { val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also {
it.setPackage(packageName) it.setPackage(packageName)
}; }
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE); val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
applicationContext.getSystemService(Context.ALARM_SERVICE); applicationContext.getSystemService(Context.ALARM_SERVICE)
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager; val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent); alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent)
} }
/* This re-starts the service on reboot; see manifest */ /* This re-starts the service on reboot; see manifest */

View file

@ -5,8 +5,6 @@ import android.app.AlertDialog
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager

View file

@ -7,6 +7,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.ACTION_VIEW import android.content.Intent.ACTION_VIEW
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
import android.view.ActionMode import android.view.ActionMode
@ -179,7 +180,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
howToExample.linksClickable = true howToExample.linksClickable = true
val howToText = getString(R.string.detail_how_to_example, topicUrl) val howToText = getString(R.string.detail_how_to_example, topicUrl)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
howToExample.text = Html.fromHtml(howToText, Html.FROM_HTML_MODE_LEGACY) howToExample.text = Html.fromHtml(howToText, Html.FROM_HTML_MODE_LEGACY)
} else { } else {
howToExample.text = Html.fromHtml(howToText) howToExample.text = Html.fromHtml(howToText)
@ -242,7 +243,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0) { if (positionStart == 0) {
Log.d(TAG, "$itemCount item(s) inserted at $positionStart, scrolling to the top") Log.d(TAG, "$itemCount item(s) inserted at 0, scrolling to the top")
mainList.scrollToPosition(positionStart) mainList.scrollToPosition(positionStart)
} }
} }
@ -572,7 +573,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener { dialog.setOnShowListener {
dialog dialog
.getButton(AlertDialog.BUTTON_POSITIVE) .getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText) .dangerButton(this)
} }
dialog.show() dialog.show()
} }
@ -610,7 +611,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener { dialog.setOnShowListener {
dialog dialog
.getButton(AlertDialog.BUTTON_POSITIVE) .getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText) .dangerButton(this)
} }
dialog.show() dialog.show()
} }
@ -620,7 +621,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
handleActionModeClick(notification) handleActionModeClick(notification)
} else if (notification.click != "") { } else if (notification.click != "") {
try { try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(notification.click))) startActivity(Intent(ACTION_VIEW, Uri.parse(notification.click)))
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Cannot open click URL", e) Log.w(TAG, "Cannot open click URL", e)
runOnUiThread { runOnUiThread {
@ -721,7 +722,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener { dialog.setOnShowListener {
dialog dialog
.getButton(AlertDialog.BUTTON_POSITIVE) .getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText) .dangerButton(this)
} }
dialog.show() dialog.show()
} }

View file

@ -27,14 +27,16 @@ import com.google.android.material.button.MaterialButton
import com.stfalcon.imageviewer.StfalconImageViewer import com.stfalcon.imageviewer.StfalconImageViewer
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadAttachmentWorker import io.heckel.ntfy.msg.DownloadAttachmentWorker
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadType import io.heckel.ntfy.msg.DownloadType
import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
import io.heckel.ntfy.util.* import io.heckel.ntfy.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class DetailAdapter(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) : class DetailAdapter(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) { ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
@ -204,7 +206,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
} }
private fun maybeRenderActions(context: Context, notification: Notification) { private fun maybeRenderActions(context: Context, notification: Notification) {
if (notification.actions != null && notification.actions.isNotEmpty()) { if (!notification.actions.isNullOrEmpty()) {
actionsWrapperView.visibility = View.VISIBLE actionsWrapperView.visibility = View.VISIBLE
val actionsCount = Math.min(notification.actions.size, 3) // per documentation, only 3 actions are available val actionsCount = Math.min(notification.actions.size, 3) // per documentation, only 3 actions are available
for (i in 0 until actionsCount) { for (i in 0 until actionsCount) {
@ -220,7 +222,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
private fun resetCardButtons() { private fun resetCardButtons() {
// clear any previously created dynamic buttons // clear any previously created dynamic buttons
actionsFlow.allViews.forEach { it -> actionsFlow.removeView(it) } actionsFlow.allViews.forEach { actionsFlow.removeView(it) }
actionsWrapperView.removeAllViews() actionsWrapperView.removeAllViews()
actionsWrapperView.addView(actionsFlow) actionsWrapperView.addView(actionsFlow)
} }
@ -371,6 +373,12 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
} }
private fun openFile(context: Context, attachment: Attachment): Boolean { private fun openFile(context: Context, attachment: Attachment): Boolean {
if (!canOpenAttachment(attachment)) {
Toast
.makeText(context, context.getString(R.string.detail_item_cannot_open_apk), Toast.LENGTH_LONG)
.show()
return true
}
Log.d(TAG, "Opening file ${attachment.contentUri}") Log.d(TAG, "Opening file ${attachment.contentUri}")
try { try {
val contentUri = Uri.parse(attachment.contentUri) val contentUri = Uri.parse(attachment.contentUri)

View file

@ -229,9 +229,8 @@ class DetailSettingsActivity : AppCompatActivity() {
return subscription.mutedUntil.toString() return subscription.mutedUntil.toString()
} }
} }
pref?.summaryProvider = Preference.SummaryProvider<ListPreference> { _ -> pref?.summaryProvider = Preference.SummaryProvider<ListPreference> {
val mutedUntilValue = subscription.mutedUntil when (val mutedUntilValue = subscription.mutedUntil) {
when (mutedUntilValue) {
Repository.MUTED_UNTIL_SHOW_ALL -> getString(R.string.settings_notifications_muted_until_show_all) Repository.MUTED_UNTIL_SHOW_ALL -> getString(R.string.settings_notifications_muted_until_show_all)
Repository.MUTED_UNTIL_FOREVER -> getString(R.string.settings_notifications_muted_until_forever) Repository.MUTED_UNTIL_FOREVER -> getString(R.string.settings_notifications_muted_until_forever)
else -> { else -> {
@ -312,7 +311,7 @@ class DetailSettingsActivity : AppCompatActivity() {
iconSetPref = findPreference(prefId) ?: return iconSetPref = findPreference(prefId) ?: return
iconSetPref.isVisible = subscription.icon == null iconSetPref.isVisible = subscription.icon == null
iconSetPref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting iconSetPref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
iconSetPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> iconSetPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
iconSetLauncher.launch("image/*") iconSetLauncher.launch("image/*")
true true
} }
@ -323,7 +322,7 @@ class DetailSettingsActivity : AppCompatActivity() {
iconRemovePref = findPreference(prefId) ?: return iconRemovePref = findPreference(prefId) ?: return
iconRemovePref.isVisible = subscription.icon != null iconRemovePref.isVisible = subscription.icon != null
iconRemovePref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting iconRemovePref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
iconRemovePref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> iconRemovePref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
iconRemovePref.isVisible = false iconRemovePref.isVisible = false
iconSetPref.isVisible = true iconSetPref.isVisible = true
deleteIcon(subscription.icon) deleteIcon(subscription.icon)

View file

@ -35,7 +35,6 @@ import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription 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.msg.ApiService import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.DownloadManager import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadType import io.heckel.ntfy.msg.DownloadType
@ -48,7 +47,6 @@ import io.heckel.ntfy.work.PollWorker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.security.SecureRandom
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.random.Random import kotlin.random.Random
@ -623,7 +621,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
dialog.setOnShowListener { dialog.setOnShowListener {
dialog dialog
.getButton(AlertDialog.BUTTON_POSITIVE) .getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText) .dangerButton(this)
} }
dialog.show() dialog.show()
} }

View file

@ -1,9 +1,7 @@
package io.heckel.ntfy.ui package io.heckel.ntfy.ui
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Color import android.graphics.Color
import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -17,10 +15,8 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.db.ConnectionState import io.heckel.ntfy.db.ConnectionState
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.readBitmapFromUriOrNull
import io.heckel.ntfy.util.displayName import io.heckel.ntfy.util.displayName
import io.heckel.ntfy.util.readBitmapFromUriOrNull
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
@ -119,7 +115,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
if (selected.contains(subscription.id)) { if (selected.contains(subscription.id)) {
itemView.setBackgroundResource(Colors.itemSelectedBackground(context)) itemView.setBackgroundResource(Colors.itemSelectedBackground(context))
} else { } else {
itemView.setBackgroundColor(Color.TRANSPARENT); itemView.setBackgroundColor(Color.TRANSPARENT)
} }
} }
} }

View file

@ -8,10 +8,8 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.up.Distributor import io.heckel.ntfy.up.Distributor
import io.heckel.ntfy.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.List
class SubscriptionsViewModel(private val repository: Repository) : ViewModel() { class SubscriptionsViewModel(private val repository: Repository) : ViewModel() {
fun list(): LiveData<List<Subscription>> { fun list(): LiveData<List<Subscription>> {

View file

@ -8,7 +8,6 @@ import android.widget.RadioButton
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View file

@ -1,7 +1,6 @@
package io.heckel.ntfy.ui package io.heckel.ntfy.ui
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
@ -295,7 +294,7 @@ class ShareActivity : AppCompatActivity() {
.show() .show()
} }
} catch (e: Exception) { } catch (e: Exception) {
val message = if (e is ApiService.UnauthorizedException) { val errorMessage = if (e is ApiService.UnauthorizedException) {
if (e.user != null) { if (e.user != null) {
getString(R.string.detail_test_message_error_unauthorized_user, e.user.username) getString(R.string.detail_test_message_error_unauthorized_user, e.user.username)
} else { } else {
@ -308,7 +307,7 @@ class ShareActivity : AppCompatActivity() {
} }
runOnUiThread { runOnUiThread {
progress.visibility = View.GONE progress.visibility = View.GONE
errorText.text = message errorText.text = errorMessage
errorImage.visibility = View.VISIBLE errorImage.visibility = View.VISIBLE
errorText.visibility = View.VISIBLE errorText.visibility = View.VISIBLE
} }

View file

@ -3,19 +3,17 @@ package io.heckel.ntfy.ui
import android.app.AlertDialog import android.app.AlertDialog
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.User import io.heckel.ntfy.db.User
import io.heckel.ntfy.util.AfterChangedTextWatcher
import io.heckel.ntfy.util.dangerButton
import io.heckel.ntfy.util.validUrl import io.heckel.ntfy.util.validUrl
class UserFragment : DialogFragment() { class UserFragment : DialogFragment() {
@ -98,28 +96,14 @@ class UserFragment : DialogFragment() {
// Delete button should be red // Delete button should be red
if (user != null) { if (user != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { dialog
dialog .getButton(AlertDialog.BUTTON_NEUTRAL)
.getButton(AlertDialog.BUTTON_NEUTRAL) .dangerButton(requireContext())
.setTextAppearance(R.style.DangerText)
} else {
dialog
.getButton(AlertDialog.BUTTON_NEUTRAL)
.setTextColor(ContextCompat.getColor(requireContext(), Colors.dangerText(requireContext())))
}
} }
// Validate input when typing // Validate input when typing
val textWatcher = object : TextWatcher { val textWatcher = AfterChangedTextWatcher {
override fun afterTextChanged(s: Editable?) { validateInput()
validateInput()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Nothing
}
} }
baseUrlView.addTextChangedListener(textWatcher) baseUrlView.addTextChangedListener(textWatcher)
usernameView.addTextChangedListener(textWatcher) usernameView.addTextChangedListener(textWatcher)
@ -140,7 +124,7 @@ class UserFragment : DialogFragment() {
} }
// Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785) // Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
return dialog return dialog
} }

View file

@ -10,12 +10,10 @@ import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.* import io.heckel.ntfy.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.util.* import java.util.*
import kotlin.random.Random
/** /**
* This is the UnifiedPush broadcast receiver to handle the distributor actions REGISTER and UNREGISTER. * This is the UnifiedPush broadcast receiver to handle the distributor actions REGISTER and UNREGISTER.

View file

@ -22,12 +22,16 @@ import android.util.Base64
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64 import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
import io.heckel.ntfy.ui.Colors
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -221,16 +225,6 @@ fun maybeAppendActionErrors(message: String, notification: Notification): String
} }
} }
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
fun fileExists(context: Context, contentUri: String?): Boolean {
return try {
fileStat(context, Uri.parse(contentUri)) // Throws if the file does not exist
true
} catch (_: Exception) {
false
}
}
// Queries the filename of a content URI // Queries the filename of a content URI
fun fileName(context: Context, contentUri: String?, fallbackName: String): String { fun fileName(context: Context, contentUri: String?, fallbackName: String): String {
return try { return try {
@ -325,6 +319,8 @@ fun formatBytes(bytes: Long, decimals: Int = 1): String {
return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current()) return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current())
} }
const val androidAppMimeType = "application/vnd.android.package-archive"
fun mimeTypeToIconResource(mimeType: String?): Int { fun mimeTypeToIconResource(mimeType: String?): Int {
return if (mimeType?.startsWith("image/") == true) { return if (mimeType?.startsWith("image/") == true) {
R.drawable.ic_file_image_red_24dp R.drawable.ic_file_image_red_24dp
@ -332,7 +328,7 @@ fun mimeTypeToIconResource(mimeType: String?): Int {
R.drawable.ic_file_video_orange_24dp R.drawable.ic_file_video_orange_24dp
} else if (mimeType?.startsWith("audio/") == true) { } else if (mimeType?.startsWith("audio/") == true) {
R.drawable.ic_file_audio_purple_24dp R.drawable.ic_file_audio_purple_24dp
} else if (mimeType == "application/vnd.android.package-archive") { } else if (mimeType == androidAppMimeType) {
R.drawable.ic_file_app_gray_24dp R.drawable.ic_file_app_gray_24dp
} else { } else {
R.drawable.ic_file_document_blue_24dp R.drawable.ic_file_document_blue_24dp
@ -343,6 +339,15 @@ fun supportedImage(mimeType: String?): Boolean {
return listOf("image/jpeg", "image/png").contains(mimeType) return listOf("image/jpeg", "image/png").contains(mimeType)
} }
// Google Play doesn't allow us to install received .apk files anymore.
// See https://github.com/binwiederhier/ntfy/issues/531
fun canOpenAttachment(attachment: Attachment?): Boolean {
if (attachment?.type == androidAppMimeType && !BuildConfig.INSTALL_PACKAGES_AVAILABLE) {
return false
}
return true
}
// Check if battery optimization is enabled, see https://stackoverflow.com/a/49098293/1440785 // Check if battery optimization is enabled, see https://stackoverflow.com/a/49098293/1440785
fun isIgnoringBatteryOptimizations(context: Context): Boolean { fun isIgnoringBatteryOptimizations(context: Context): Boolean {
val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
@ -494,3 +499,11 @@ fun String.sha256(): String {
val digest = md.digest(this.toByteArray()) val digest = md.digest(this.toByteArray())
return digest.fold("") { str, it -> str + "%02x".format(it) } return digest.fold("") { str, it -> str + "%02x".format(it) }
} }
fun Button.dangerButton(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setTextAppearance(R.style.DangerText)
} else {
setTextColor(ContextCompat.getColor(context, Colors.dangerText(context)))
}
}

View file

@ -327,4 +327,6 @@
<string name="detail_settings_about_header">Относно</string> <string name="detail_settings_about_header">Относно</string>
<string name="detail_settings_about_topic_url_title">Адрес на темата</string> <string name="detail_settings_about_topic_url_title">Адрес на темата</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Копирано в междинната памет</string> <string name="detail_settings_about_topic_url_copied_to_clipboard_message">Копирано в междинната памет</string>
<string name="main_menu_donate_title">Даряване 💸</string>
<string name="detail_item_cannot_open_apk">Ntfy не може да инсталира получени приложения. Вместо това изтеглете чрез браузъра. За подробности вижте дефект №531.</string>
</resources> </resources>

View file

@ -6,4 +6,43 @@
<string name="channel_notifications_high_name">Notificacions (alta prioritat)</string> <string name="channel_notifications_high_name">Notificacions (alta prioritat)</string>
<string name="channel_notifications_max_name">Notificacions (prioritat màx)</string> <string name="channel_notifications_max_name">Notificacions (prioritat màx)</string>
<string name="channel_subscriber_service_name">Servei Subscripció</string> <string name="channel_subscriber_service_name">Servei Subscripció</string>
<string name="main_menu_donate_title">Donar 💸</string>
<string name="main_item_status_reconnecting">reconnectant…</string>
<string name="channel_subscriber_notification_title">Escoltant notificacions entrants</string>
<string name="channel_subscriber_notification_instant_text">Subscrit per entrega instantània de temes</string>
<string name="channel_subscriber_notification_instant_text_one">Subscrit per entrega instantània d\'un tema</string>
<string name="channel_subscriber_notification_instant_text_two">Subscrit per dues entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_three">Subscrit per tres entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_four">Subscrit per quatre entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_five">Subscrit per cinc entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_six">Subscrit per sis entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_more">Subscrit per %1$d entregues instantànies de temes</string>
<string name="channel_subscriber_notification_noinstant_text">Subscrit als temes</string>
<string name="channel_subscriber_notification_noinstant_text_one">Subscrit a un tema</string>
<string name="channel_subscriber_notification_noinstant_text_two">Subscrit a dos temes</string>
<string name="channel_subscriber_notification_noinstant_text_three">Subscrit a tres temes</string>
<string name="channel_subscriber_notification_noinstant_text_six">Subscrit a sis temes</string>
<string name="main_action_mode_delete_dialog_permanently_delete">Eliminat permanentment</string>
<string name="main_action_mode_delete_dialog_cancel">Cancel·lar</string>
<string name="main_item_status_text_one">%1$d notificació</string>
<string name="channel_subscriber_notification_noinstant_text_more">Subscrit a %1$d temes</string>
<string name="channel_subscriber_notification_noinstant_text_four">Subscrit a quatre temes</string>
<string name="channel_subscriber_notification_noinstant_text_five">Subscrit a cinc temes</string>
<string name="refresh_message_result">%1$d notificacions rebudes</string>
<string name="refresh_message_no_results">Tot està actualitzat</string>
<string name="refresh_message_error">No es poden actualitzar %1$d subscripciones
\n
\n%2$s</string>
<string name="refresh_message_error_one">No s\'ha pogut actualitzar la subscripció: %1$s</string>
<string name="main_action_bar_title">Temes subscrits</string>
<string name="main_menu_notifications_enabled">Notificacions activades</string>
<string name="main_menu_notifications_disabled_forever">Notificacions silenciades</string>
<string name="main_menu_notifications_disabled_until">Notificacions silenciades fins: %1$s</string>
<string name="main_menu_settings_title">Ajustos</string>
<string name="main_menu_report_bug_title">Reportar un problema</string>
<string name="main_menu_docs_title">Llegir la documentació</string>
<string name="main_menu_rate_title">Valora la aplicació ⭐</string>
<string name="main_action_mode_menu_unsubscribe">Donar-se de baixa</string>
<string name="main_action_mode_delete_dialog_message">Donar-se de baixa del tema(es) seleccionat(s) permanentment i eliminar totes les notificacions\?</string>
<string name="main_item_status_text_not_one">%1$d notificacions</string>
</resources> </resources>

View file

@ -327,4 +327,6 @@
<string name="main_banner_websocket_button_enable_now">Povolit nyní</string> <string name="main_banner_websocket_button_enable_now">Povolit nyní</string>
<string name="main_banner_websocket_text">WebSockets jsou doporučenou metodou připojení k vašemu serveru, která může zlepšit zvýšit výdrž baterie, ale může vyžadovat <a href="https://ntfy.sh/docs/config/#nginxapache2caddy">další konfiguraci v proxy serveru</a>. Metodu připojení lze přepnout v Nastavení.</string> <string name="main_banner_websocket_text">WebSockets jsou doporučenou metodou připojení k vašemu serveru, která může zlepšit zvýšit výdrž baterie, ale může vyžadovat <a href="https://ntfy.sh/docs/config/#nginxapache2caddy">další konfiguraci v proxy serveru</a>. Metodu připojení lze přepnout v Nastavení.</string>
<string name="add_dialog_base_urls_dropdown_choose">Zvolit URL služby</string> <string name="add_dialog_base_urls_dropdown_choose">Zvolit URL služby</string>
<string name="main_menu_donate_title">Přispět 💸</string>
<string name="detail_item_cannot_open_apk">Aplikace již nelze nainstalovat. Místo toho stahujte přes prohlížeč. Podrobnosti naleznete v issue #531.</string>
</resources> </resources>

View file

@ -309,7 +309,7 @@
<string name="detail_settings_appearance_header">Darstellung</string> <string name="detail_settings_appearance_header">Darstellung</string>
<string name="detail_settings_appearance_icon_set_title">Abo-Icon</string> <string name="detail_settings_appearance_icon_set_title">Abo-Icon</string>
<string name="detail_settings_appearance_icon_set_summary">Ein Icon zur Darstellung in Benachrichtigungen auswählen</string> <string name="detail_settings_appearance_icon_set_summary">Ein Icon zur Darstellung in Benachrichtigungen auswählen</string>
<string name="detail_settings_appearance_icon_remove_title">Abo-Icon (entfernen durch antippen)</string> <string name="detail_settings_appearance_icon_remove_title">Abo-Icon (entfernen durch Antippen)</string>
<string name="detail_settings_appearance_icon_error_saving">Kann Icon nicht speichern: %1$s</string> <string name="detail_settings_appearance_icon_error_saving">Kann Icon nicht speichern: %1$s</string>
<string name="detail_settings_global_setting_title">Globale Einstellung verwenden</string> <string name="detail_settings_global_setting_title">Globale Einstellung verwenden</string>
<string name="detail_settings_global_setting_suffix">globale Einstellung</string> <string name="detail_settings_global_setting_suffix">globale Einstellung</string>
@ -323,8 +323,10 @@
<string name="add_dialog_base_urls_dropdown_clear">Service-URL löschen</string> <string name="add_dialog_base_urls_dropdown_clear">Service-URL löschen</string>
<string name="detail_settings_appearance_display_name_default_summary">%1$s (Standard)</string> <string name="detail_settings_appearance_display_name_default_summary">%1$s (Standard)</string>
<string name="detail_settings_appearance_display_name_title">Anzeigename</string> <string name="detail_settings_appearance_display_name_title">Anzeigename</string>
<string name="detail_settings_appearance_display_name_message">Gib einen eigenen Anzeigenamen für diese Abo an. Leer lassen für den Standardwert (%1$s).</string> <string name="detail_settings_appearance_display_name_message">Gib einen eigenen Anzeigenamen für dieses Abo an. Leer lassen für den Standardwert (%1$s).</string>
<string name="detail_settings_about_topic_url_title">Themen-URL</string> <string name="detail_settings_about_topic_url_title">Themen-URL</string>
<string name="detail_settings_about_header">Über</string> <string name="detail_settings_about_header">Über</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">In Zwischenablage kopiert</string> <string name="detail_settings_about_topic_url_copied_to_clipboard_message">In Zwischenablage kopiert</string>
</resources> <string name="main_menu_donate_title">Spenden 💸</string>
<string name="detail_item_cannot_open_apk">Apps können nicht mehr installiert werden. Bitte stattdessen über einen Browser herunterladen. Details siehe Issue #531.</string>
</resources>

View file

@ -327,4 +327,6 @@
<string name="add_dialog_base_urls_dropdown_clear">Borrar la URL del servicio</string> <string name="add_dialog_base_urls_dropdown_clear">Borrar la URL del servicio</string>
<string name="main_banner_websocket_text">Cambiar a WebSockets es la forma recomendada para conectarse a su servidor, y podría mejorar la vida de la batería, pero puede requerir <a href="https://ntfy.sh/docs/config/#nginxapache2caddy">configuración adicional en su proxy</a>. Esto se puede cambiar en la Configuración.</string> <string name="main_banner_websocket_text">Cambiar a WebSockets es la forma recomendada para conectarse a su servidor, y podría mejorar la vida de la batería, pero puede requerir <a href="https://ntfy.sh/docs/config/#nginxapache2caddy">configuración adicional en su proxy</a>. Esto se puede cambiar en la Configuración.</string>
<string name="main_banner_websocket_button_enable_now">Habilitar ahora</string> <string name="main_banner_websocket_button_enable_now">Habilitar ahora</string>
<string name="main_menu_donate_title">Donar 💸</string>
<string name="detail_item_cannot_open_apk">Las aplicaciones ya no se pueden instalar desde ntfy. Descárguelas a través del navegador. Consulte el issue #531 para obtener más información.</string>
</resources> </resources>

View file

@ -6,7 +6,7 @@
<string name="channel_notifications_min_name">Notifikasi (prioritas min)</string> <string name="channel_notifications_min_name">Notifikasi (prioritas min)</string>
<string name="channel_notifications_low_name">Notifikasi (prioritas rendah)</string> <string name="channel_notifications_low_name">Notifikasi (prioritas rendah)</string>
<string name="channel_subscriber_service_name">Layanan Langganan</string> <string name="channel_subscriber_service_name">Layanan Langganan</string>
<string name="channel_subscriber_notification_title">Mendengarkan untuk notifikasi masuk</string> <string name="channel_subscriber_notification_title">Mendengarkan notifikasi masuk</string>
<string name="channel_subscriber_notification_instant_text">Berlangganan ke topik pengiriman instan</string> <string name="channel_subscriber_notification_instant_text">Berlangganan ke topik pengiriman instan</string>
<string name="channel_subscriber_notification_instant_text_one">Berlangganan ke satu topik pengiriman instan</string> <string name="channel_subscriber_notification_instant_text_one">Berlangganan ke satu topik pengiriman instan</string>
<string name="channel_subscriber_notification_instant_text_two">Berlangganan ke dua topik pengiriman instan</string> <string name="channel_subscriber_notification_instant_text_two">Berlangganan ke dua topik pengiriman instan</string>
@ -23,7 +23,7 @@
<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 bug</string> <string name="main_menu_report_bug_title">Laporkan kutu</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>
@ -327,4 +327,6 @@
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Disalin ke papan klip</string> <string name="detail_settings_about_topic_url_copied_to_clipboard_message">Disalin ke papan klip</string>
<string name="detail_settings_about_header">Tentang</string> <string name="detail_settings_about_header">Tentang</string>
<string name="detail_settings_about_topic_url_title">URL Topik</string> <string name="detail_settings_about_topic_url_title">URL Topik</string>
<string name="main_menu_donate_title">Donasi 💸</string>
<string name="detail_item_cannot_open_apk">Aplikasi tidak dapat dipasang lagi. Unduh melalui peramban. Lihat masalah #531 untuk detail lebih lanjut.</string>
</resources> </resources>

View file

@ -15,7 +15,7 @@
<string name="main_action_mode_delete_dialog_permanently_delete">מחק/י לצמיתות</string> <string name="main_action_mode_delete_dialog_permanently_delete">מחק/י לצמיתות</string>
<string name="main_action_mode_delete_dialog_cancel">ביטול</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_one">התראת %1$d</string>
<string name="main_item_status_text_not_one">התראות %1$d</string> <string name="main_item_status_text_not_one">%1$d התראות</string>
<string name="main_item_date_yesterday">אתמול</string> <string name="main_item_date_yesterday">אתמול</string>
<string name="main_add_button_description">הוספת רישום</string> <string name="main_add_button_description">הוספת רישום</string>
<string name="main_no_subscriptions_text">נראה שלא נרשמת לאף נושא עדיין.</string> <string name="main_no_subscriptions_text">נראה שלא נרשמת לאף נושא עדיין.</string>
@ -39,4 +39,20 @@
<string name="main_item_status_reconnecting">מתחבר מחדש…</string> <string name="main_item_status_reconnecting">מתחבר מחדש…</string>
<string name="main_how_to_intro">לחצ\\י על + על מנת ליצור או להירשם אל מול נושא מסוים. לאחר מכן תקבל\\י התראות במכשירך כשתשלח\\י התראות דרך PUT או POST.</string> <string name="main_how_to_intro">לחצ\\י על + על מנת ליצור או להירשם אל מול נושא מסוים. לאחר מכן תקבל\\י התראות במכשירך כשתשלח\\י התראות דרך PUT או POST.</string>
<string name="main_how_to_link">הוראות מפורטות זמינות ב-ntfy.sh, ובדוקומנטציה.</string> <string name="main_how_to_link">הוראות מפורטות זמינות ב-ntfy.sh, ובדוקומנטציה.</string>
</resources> <string name="main_menu_donate_title">תרום 💸</string>
<string name="channel_subscriber_notification_instant_text_two">רשום לשני נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_three">רשום לשלושה נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text">רשום לנושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_one">רשום לנושא אחד במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_four">רשום לארבעה נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_five">רשום לחמישה נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_six">רשום לשישה נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_more">רשום ל%1$d נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_noinstant_text">רשום לנושאים</string>
<string name="channel_subscriber_notification_noinstant_text_one">רשום לנושא אחד</string>
<string name="channel_subscriber_notification_noinstant_text_two">רשום לשני נושאים</string>
<string name="channel_subscriber_notification_noinstant_text_three">רשום לשלושה נושאים</string>
<string name="channel_subscriber_notification_noinstant_text_four">רשום לארבעה נושאים</string>
<string name="channel_subscriber_notification_noinstant_text_five">רשום לחמישה נושאים</string>
<string name="channel_subscriber_notification_noinstant_text_six">רשום לשישה נושאים</string>
</resources>

View file

@ -327,4 +327,6 @@
<string name="detail_settings_about_header">About</string> <string name="detail_settings_about_header">About</string>
<string name="detail_settings_about_topic_url_title">トピックのURL</string> <string name="detail_settings_about_topic_url_title">トピックのURL</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">クリップボードにコピーしました</string> <string name="detail_settings_about_topic_url_copied_to_clipboard_message">クリップボードにコピーしました</string>
<string name="main_menu_donate_title">寄付する💸</string>
<string name="detail_item_cannot_open_apk">アプリはインストールできなくなりました。代替手段としてブラウザからダウンロードしてください。詳細は issue #531 をご参照ください。</string>
</resources> </resources>

View file

@ -77,7 +77,7 @@
<string name="detail_item_tags">Tags: %1$s</string> <string name="detail_item_tags">Tags: %1$s</string>
<string name="detail_item_snack_deleted">Notificação deletada</string> <string name="detail_item_snack_deleted">Notificação deletada</string>
<string name="detail_item_snack_undo">Desfazer</string> <string name="detail_item_snack_undo">Desfazer</string>
<string name="detail_item_menu_download">Fazer download do arquivo</string> <string name="detail_item_menu_download">Baixar arquivo</string>
<string name="detail_item_menu_cancel">Cancelar o download</string> <string name="detail_item_menu_cancel">Cancelar o download</string>
<string name="detail_item_cannot_open">Não foi possível abrir o anexo: %1$s</string> <string name="detail_item_cannot_open">Não foi possível abrir o anexo: %1$s</string>
<string name="detail_item_cannot_open_not_found">Não foi possível abrir o anexo: O arquivo pode ter sido deletado, ou não existe app instalado que consiga abrir o arquivo.</string> <string name="detail_item_cannot_open_not_found">Não foi possível abrir o anexo: O arquivo pode ter sido deletado, ou não existe app instalado que consiga abrir o arquivo.</string>
@ -258,4 +258,74 @@
<string name="settings_general_dark_mode_summary_light">Modo claro</string> <string name="settings_general_dark_mode_summary_light">Modo claro</string>
<string name="settings_general_dark_mode_title">Modo escuro</string> <string name="settings_general_dark_mode_title">Modo escuro</string>
<string name="settings_general_dark_mode_summary_system">Usar o padrão do sistema</string> <string name="settings_general_dark_mode_summary_system">Usar o padrão do sistema</string>
<string name="main_menu_donate_title">Doar 💸</string>
<string name="settings_backup_restore_restore_failed">Recuperação falhou %1$s</string>
<string name="settings_advanced_header">Avançado</string>
<string name="settings_advanced_broadcast_title">Messagens de broadcast</string>
<string name="settings_advanced_broadcast_summary_disabled">Os aplicativos não podem receber notificações por broadcast</string>
<string name="settings_advanced_broadcast_summary_enabled">Os aplicativos já podem receber notificações por broadcast</string>
<string name="settings_advanced_export_logs_entry_copy_original">Copiar para área de transferência</string>
<string name="settings_advanced_export_logs_entry_copy_scrubbed">Copiar para área de transferência (censurado)</string>
<string name="settings_advanced_export_logs_entry_upload_original">Carregar e copiar link</string>
<string name="settings_advanced_export_logs_uploading">Carregando logs …</string>
<string name="settings_advanced_export_logs_copied_url">Logs enviados e URL copiada</string>
<string name="settings_advanced_connection_protocol_summary_jsonhttp">Use um stream de JSON através de HTTP para se conectar ao servidor. Este método foi testado na pratica, mas pode consumir mais bateria.</string>
<string name="settings_about_version_format">ntfy %1$s (%2$s)</string>
<string name="detail_settings_notifications_instant_summary_off">As notificações são entregues utilizando o Firebase. A entrega pode atrasar, mas consome menos bateria.</string>
<string name="detail_settings_appearance_header">Aparência</string>
<string name="detail_settings_appearance_icon_set_title">Ícone de assinatura</string>
<string name="detail_settings_appearance_icon_set_summary">Defina um ícone para ser exibido nas notificações</string>
<string name="detail_settings_appearance_icon_remove_title">Ícone de assinatura (toque para remover)</string>
<string name="detail_settings_appearance_display_name_message">Defina um nome de exibição personalizado para esta assinatura. Deixe em branco para o padrão (%1$s).</string>
<string name="detail_settings_appearance_display_name_default_summary">%1$s (padrão)</string>
<string name="detail_settings_global_setting_title">Usar configuração global</string>
<string name="detail_settings_global_setting_suffix">usar configuração global</string>
<string name="detail_settings_about_header">Sobre</string>
<string name="detail_settings_about_topic_url_title">URL do tópico</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Copiado para a área de transferência</string>
<string name="user_dialog_title_add">Adicionar usuário</string>
<string name="settings_backup_restore_restore_title">Recuperar do arquivo</string>
<string name="settings_backup_restore_restore_summary">Importar configurações, notificações e usuários</string>
<string name="settings_backup_restore_restore_successful">Restauração concluída</string>
<string name="settings_advanced_record_logs_summary_enabled">Registros (ate 1000 registros) para o dispositivo …</string>
<string name="settings_advanced_record_logs_summary_disabled">Ative o log para que seja possível compartilhar mais tarde os registros para diagnostico.</string>
<string name="settings_advanced_record_logs_title">Logs de registros</string>
<string name="settings_advanced_export_logs_title">Copiar/carregar logs</string>
<string name="settings_advanced_export_logs_summary">Copie os logs para a área de transferência ou faça o upload para nopaste.net (propriedade do autor do ntfy). Hostnames e tópicos podem ser censurados, as notificações não.</string>
<string name="settings_advanced_export_logs_entry_upload_scrubbed">Carregar e copiar link (censurado)</string>
<string name="settings_advanced_export_logs_copied_logs">Logs copiado para área de transferência</string>
<string name="settings_advanced_export_logs_scrub_dialog_text">Esses tópicos/hostnames foram substituídos por nomes de frutas, então você pode compartilhar o log sem se preocupar:
\n
\n%1$s
\n
\nAs senhas são retiradas e não são listadas aqui.</string>
<string name="settings_advanced_export_logs_error_uploading">Não foi possível carregar os logs: %1$s</string>
<string name="settings_advanced_export_logs_scrub_dialog_button_ok">OK</string>
<string name="settings_advanced_export_logs_scrub_dialog_empty">Nenhum tópico/hostname foi editado. Talvez você não tenha nenhuma assinatura\?</string>
<string name="settings_advanced_clear_logs_title">Limpar logs</string>
<string name="settings_advanced_clear_logs_deleted_toast">Logs excluídos</string>
<string name="settings_advanced_connection_protocol_title">Protocolo de conexão</string>
<string name="settings_advanced_clear_logs_summary">Exclua os logs gravados anteriormente e comece de novo</string>
<string name="settings_advanced_connection_protocol_summary_ws">Use WebSockets para se conectar ao servidor. Este é o método recomendado, mas pode exigir configuração adicional em seu proxy.</string>
<string name="settings_advanced_connection_protocol_entry_jsonhttp">Stream JSON através de HTTP</string>
<string name="settings_about_header">Sobre</string>
<string name="settings_about_version_title">Versão</string>
<string name="settings_advanced_connection_protocol_entry_ws">Web Sockets</string>
<string name="detail_settings_notifications_instant_title">Entrega instantânea</string>
<string name="detail_settings_notifications_instant_summary_on">As notificações são entregues instantaneamente. Requer um serviço que roda em primeiro plano e consome mais bateria.</string>
<string name="settings_about_version_copied_to_clipboard_message">Copiado para área de transferência</string>
<string name="detail_settings_appearance_icon_error_saving">Não foi possível salvar o ícone: %1$s</string>
<string name="detail_settings_appearance_icon_remove_summary">Ícone exibido nas notificações deste tópico</string>
<string name="detail_settings_appearance_display_name_title">Nome de exibição</string>
<string name="user_dialog_description_edit">Você pode editar o nome de usuário/senha do usuário selecionado ou excluí-lo.</string>
<string name="user_dialog_base_url_hint">URL do serviço</string>
<string name="user_dialog_title_edit">Editar usuário</string>
<string name="user_dialog_description_add">Você pode adicionar um usuário aqui. Todos os tópicos para o servidor fornecido usarão esse usuário.</string>
<string name="user_dialog_username_hint">Nome do usuário</string>
<string name="user_dialog_password_hint_add">Senha</string>
<string name="user_dialog_password_hint_edit">Senha (se deixada em branco não será alterada)</string>
<string name="user_dialog_button_cancel">Cancelar</string>
<string name="user_dialog_button_add">Adicionar usuário</string>
<string name="user_dialog_button_delete">Deletar usuário</string>
<string name="user_dialog_button_save">Salvar</string>
</resources> </resources>

View file

@ -99,4 +99,28 @@
<string name="add_dialog_login_username_hint">Användarnamn</string> <string name="add_dialog_login_username_hint">Användarnamn</string>
<string name="add_dialog_login_password_hint">Lösenord</string> <string name="add_dialog_login_password_hint">Lösenord</string>
<string name="detail_test_message_error_unauthorized_anon">Kan inte skicka meddelande: Anonym publicering är inte tillåten.</string> <string name="detail_test_message_error_unauthorized_anon">Kan inte skicka meddelande: Anonym publicering är inte tillåten.</string>
<string name="detail_item_snack_deleted">Notifikation borttagen</string>
<string name="detail_item_menu_copy_url_copied">URL kopierad till urklipp</string>
<string name="detail_item_menu_copy_contents">Kopiera notifikation</string>
<string name="detail_item_menu_copy_contents_copied">Notifikation kopierad till urklipp</string>
<string name="detail_item_cannot_open_url">Kan inte öppna URL: %1$s</string>
<string name="detail_menu_clear">Rensa alla notifikationer</string>
<string name="detail_menu_test">Skicka testnotifikation</string>
<string name="detail_action_mode_menu_copy">Kopiera</string>
<string name="detail_action_mode_menu_delete">Ta bort</string>
<string name="detail_action_mode_delete_dialog_permanently_delete">Ta bort permanent</string>
<string name="detail_action_mode_delete_dialog_cancel">Avbryt</string>
<string name="detail_settings_title">Prenumerationsinställningar</string>
<string name="share_title">Dela</string>
<string name="share_menu_send">Dela</string>
<string name="main_menu_donate_title">Donera 💸</string>
<string name="detail_item_snack_undo">Ångra</string>
<string name="detail_item_menu_open">Öppna fil</string>
<string name="detail_item_menu_delete">Ta bort fil</string>
<string name="detail_item_menu_download">Ladda ner fil</string>
<string name="detail_item_menu_cancel">Avbryt nedladdning</string>
<string name="detail_item_menu_save_file">Spara fil</string>
<string name="detail_item_menu_copy_url">Kopiera URL</string>
<string name="detail_item_download_info_download_failed">nedladdning misslyckad</string>
<string name="detail_menu_settings">Prenumerationsinställningar</string>
</resources> </resources>

View file

@ -327,4 +327,6 @@
<string name="detail_settings_about_header">Hakkında</string> <string name="detail_settings_about_header">Hakkında</string>
<string name="detail_settings_about_topic_url_title">Konu URL\'si</string> <string name="detail_settings_about_topic_url_title">Konu URL\'si</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Panoya kopyalandı</string> <string name="detail_settings_about_topic_url_copied_to_clipboard_message">Panoya kopyalandı</string>
<string name="main_menu_donate_title">Bağış yap 💸</string>
<string name="detail_item_cannot_open_apk">Uygulamalar artık kurulamıyor. Bunun yerine tarayıcı üzerinden indirin. Ayrıntılar için sorun #531\'e bakın.</string>
</resources> </resources>

View file

@ -327,4 +327,6 @@
<string name="detail_settings_about_header">关于</string> <string name="detail_settings_about_header">关于</string>
<string name="detail_settings_about_topic_url_title">话题 URL</string> <string name="detail_settings_about_topic_url_title">话题 URL</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">已复制到剪贴板</string> <string name="detail_settings_about_topic_url_copied_to_clipboard_message">已复制到剪贴板</string>
<string name="main_menu_donate_title">捐赠 💸</string>
<string name="detail_item_cannot_open_apk">无法再安装应用。 请通过浏览器下载。 有关详细信息,请参阅问题 #531。</string>
</resources> </resources>

View file

@ -1,23 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="channel_subscriber_notification_noinstant_text_six">已訂閱6個主題</string> <string name="channel_subscriber_notification_noinstant_text_six">已訂閱 6 個主題</string>
<string name="channel_notifications_default_name">通知(預設優先)</string> <string name="channel_notifications_default_name">通知 (預設優先)</string>
<string name="main_menu_report_bug_title">回報bug</string> <string name="main_menu_report_bug_title">問題回報</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="channel_subscriber_notification_instant_text_one">已訂閱1個即時推送主題</string> <string name="channel_subscriber_notification_instant_text_one">已訂閱 1 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_two">已訂閱2個即時推送主題</string> <string name="channel_subscriber_notification_instant_text_two">已訂閱 2 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_four">已訂閱4個即時推送主題</string> <string name="channel_subscriber_notification_instant_text_four">已訂閱 4 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_five">已訂閱5個即時推送主題</string> <string name="channel_subscriber_notification_instant_text_five">已訂閱 5 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_six">已訂閱6個即時推送主題</string> <string name="channel_subscriber_notification_instant_text_six">已訂閱 6 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_more">已訂閱%1$d個即時推送主題</string> <string name="channel_subscriber_notification_instant_text_more">已訂閱 %1$d 個即時推送主題</string>
<string name="channel_subscriber_notification_noinstant_text">已訂閱主題</string> <string name="channel_subscriber_notification_noinstant_text">已訂閱主題</string>
<string name="refresh_message_result">收到%1$d個通知</string> <string name="refresh_message_result">收到 %1$d 個通知</string>
<string name="refresh_message_no_results">已同步到最新</string> <string name="refresh_message_no_results">已同步到最新</string>
<string name="refresh_message_error">有%1$d個訂閱無法更新 <string name="refresh_message_error"> %1$d 個訂閱無法更新
\n \n
\n%2$s</string> \n%2$s</string>
<string name="refresh_message_error_one">訂閱無法更新:%1$s</string> <string name="refresh_message_error_one">訂閱無法更新 %1$s</string>
<string name="main_action_bar_title">已訂閱主題</string> <string name="main_action_bar_title">已訂閱主題</string>
<string name="main_menu_notifications_enabled">通知已開啟</string> <string name="main_menu_notifications_enabled">通知已開啟</string>
<string name="main_menu_notifications_disabled_forever">通知已靜音</string> <string name="main_menu_notifications_disabled_forever">通知已靜音</string>
@ -27,8 +27,8 @@
<string name="main_action_mode_delete_dialog_message">取消訂閱已選取的主題且永久刪除所有通知?</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_permanently_delete">永久刪除</string>
<string name="main_action_mode_delete_dialog_cancel">取消</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_one">%1$d 個通知</string>
<string name="main_item_status_text_not_one">%1$d個通知</string> <string name="main_item_status_text_not_one">%1$d 個通知</string>
<string name="main_item_date_yesterday">昨天</string> <string name="main_item_date_yesterday">昨天</string>
<string name="main_add_button_description">新增訂閱</string> <string name="main_add_button_description">新增訂閱</string>
<string name="main_no_subscriptions_text">看來你還沒有訂閱任何主題。</string> <string name="main_no_subscriptions_text">看來你還沒有訂閱任何主題。</string>
@ -36,7 +36,7 @@
<string name="main_how_to_link">更多資訊請上 ntfy.shdocs會有更多說明。</string> <string name="main_how_to_link">更多資訊請上 ntfy.shdocs會有更多說明。</string>
<string name="main_unified_push_toast">此訂閱已由 %1$s 透過 UnifiedPush 管理</string> <string name="main_unified_push_toast">此訂閱已由 %1$s 透過 UnifiedPush 管理</string>
<string name="main_item_status_unified_push">%1$s (UnifiedPush)</string> <string name="main_item_status_unified_push">%1$s (UnifiedPush)</string>
<string name="main_banner_battery_text">為了避免通知傳送問題,電池最佳化必須關閉</string> <string name="main_banner_battery_text">為了避免通知傳送問題,請務必關閉電池最佳化功能</string>
<string name="main_banner_battery_button_remind_later">稍後詢問我</string> <string name="main_banner_battery_button_remind_later">稍後詢問我</string>
<string name="main_banner_battery_button_dismiss">略過</string> <string name="main_banner_battery_button_dismiss">略過</string>
<string name="main_banner_battery_button_fix_now">立即修正</string> <string name="main_banner_battery_button_fix_now">立即修正</string>
@ -47,35 +47,147 @@
<string name="add_dialog_description_below">因為主題不能受密碼保護,請盡量取一個難以猜測的主題名稱。在訂閱之後你就可以使用 PUT/POST 來發送通知。</string> <string name="add_dialog_description_below">因為主題不能受密碼保護,請盡量取一個難以猜測的主題名稱。在訂閱之後你就可以使用 PUT/POST 來發送通知。</string>
<string name="add_dialog_use_another_server">使用其他伺服器</string> <string name="add_dialog_use_another_server">使用其他伺服器</string>
<string name="add_dialog_use_another_server_description">在下方輸入自訂的網址來訂閱主題。</string> <string name="add_dialog_use_another_server_description">在下方輸入自訂的網址來訂閱主題。</string>
<string name="channel_notifications_min_name">通知(最低優先)</string> <string name="channel_notifications_min_name">通知 (最低優先)</string>
<string name="channel_notifications_low_name">通知(低優先)</string> <string name="channel_notifications_low_name">通知 (低優先)</string>
<string name="channel_notifications_high_name">通知(高優先)</string> <string name="channel_notifications_high_name">通知 (高優先)</string>
<string name="channel_notifications_max_name">通知(最高優先)</string> <string name="channel_notifications_max_name">通知 (最高優先)</string>
<string name="channel_subscriber_service_name">訂閱服務</string> <string name="channel_subscriber_service_name">訂閱服務</string>
<string name="channel_subscriber_notification_instant_text_three">已訂閱3個即時推送主題</string> <string name="channel_subscriber_notification_instant_text_three">已訂閱 3 個即時推送主題</string>
<string name="main_menu_docs_title">閱讀文件</string> <string name="main_menu_docs_title">閱讀技術文件</string>
<string name="main_banner_websocket_text">建議使用 WebSockets 來連線你的伺服器,此動作可以有效增加電池續航,但需要<a href="https://ntfy.sh/docs/config/#nginxapache2caddy">對proxy進行更多設定</a>。這個動作可以在設定中進行</string> <string name="main_banner_websocket_text">建議使用 WebSocket 來接收通知。這個連接方式能有效改善電池續航,但可能需要<a href="https://ntfy.sh/docs/config/#nginxapache2caddy">在伺服器端進行額外設定</a>。你可以在設定頁面更改不同的連接方式</string>
<string name="channel_subscriber_notification_noinstant_text_three">已訂閱3個主題</string> <string name="channel_subscriber_notification_noinstant_text_three">已訂閱 3 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_more">已訂閱%1$d個主題</string> <string name="channel_subscriber_notification_noinstant_text_more">已訂閱 %1$d 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_one">已訂閱1個主題</string> <string name="channel_subscriber_notification_noinstant_text_one">已訂閱 1 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_two">已訂閱2個主題</string> <string name="channel_subscriber_notification_noinstant_text_two">已訂閱 2 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_four">已訂閱4個主題</string> <string name="channel_subscriber_notification_noinstant_text_four">已訂閱 4 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_five">已訂閱5個主題</string> <string name="channel_subscriber_notification_noinstant_text_five">已訂閱 5 個主題</string>
<string name="main_item_status_reconnecting">重新連線中 </string> <string name="main_item_status_reconnecting">重新連線中…</string>
<string name="main_menu_notifications_disabled_until">通知靜音到%1$s</string> <string name="main_menu_notifications_disabled_until">通知靜音到 %1$s</string>
<string name="add_dialog_topic_name_hint">主題名稱(例如:phils_alerts)</string> <string name="add_dialog_topic_name_hint">主題名稱 (例如:phils_alerts)</string>
<string name="add_dialog_button_cancel">取消</string> <string name="add_dialog_button_cancel">取消</string>
<string name="add_dialog_button_subscribe">訂閱</string> <string name="add_dialog_button_subscribe">訂閱</string>
<string name="add_dialog_button_back">退</string> <string name="add_dialog_button_back"></string>
<string name="add_dialog_error_connection_failed">連接失敗: %1$s</string> <string name="add_dialog_error_connection_failed">連接失敗 %1$s</string>
<string name="add_dialog_login_title">需要登入</string> <string name="add_dialog_login_title">需要登入</string>
<string name="add_dialog_login_username_hint">使用者名</string> <string name="add_dialog_login_username_hint">使用者名</string>
<string name="add_dialog_login_password_hint">密碼</string> <string name="add_dialog_login_password_hint">密碼</string>
<string name="add_dialog_login_error_not_authorized">登入失敗. 使用者%1$s 未獲得授權.</string> <string name="add_dialog_login_error_not_authorized">登入失敗,使用者 %1$s 並未授權訂閱這個主題。</string>
<string name="add_dialog_login_new_user">新使用者</string> <string name="add_dialog_login_new_user">建立新使用者</string>
<string name="add_dialog_base_urls_dropdown_choose">選擇服務網址</string> <string name="add_dialog_base_urls_dropdown_choose">選擇 ntfy 服務 URL</string>
<string name="add_dialog_base_urls_dropdown_clear">清除服務網址</string> <string name="add_dialog_base_urls_dropdown_clear">清除 URL</string>
<string name="detail_copied_to_clipboard_message">複製到剪貼簿</string> <string name="detail_copied_to_clipboard_message">複製到剪貼簿</string>
<string name="add_dialog_button_login">登入</string> <string name="add_dialog_button_login">登入</string>
<string name="add_dialog_login_description">這篇主題要求登入. 請輸入帳號密碼.</string> <string name="add_dialog_login_description">這個主題需要登入,請先輸入帳號密碼。</string>
<string name="add_dialog_instant_delivery">在省電模式下依然接收即時通知</string>
<string name="add_dialog_instant_delivery_description">確保通知能在未使用裝置時都能接收。</string>
<string name="detail_clear_dialog_cancel">取消</string>
<string name="detail_delete_dialog_cancel">取消</string>
<string name="detail_action_mode_delete_dialog_cancel">取消</string>
<string name="share_title">分享</string>
<string name="share_topic_title">分享到</string>
<string name="settings_notifications_priority_low"></string>
<string name="settings_notifications_priority_default">預設</string>
<string name="settings_notifications_priority_high"></string>
<string name="settings_notifications_priority_max">最高</string>
<string name="settings_notifications_auto_delete_summary_one_month">自動刪除 1 個月前的通知</string>
<string name="settings_notifications_auto_delete_one_day">1 天後</string>
<string name="settings_notifications_auto_delete_three_days">3 天後</string>
<string name="settings_notifications_auto_delete_one_week">1 週後</string>
<string name="settings_notifications_auto_delete_one_month">1 個月後</string>
<string name="settings_notifications_auto_delete_three_months">3 個月後</string>
<string name="settings_general_header">一般</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_users_title">管理使用者</string>
<string name="settings_backup_restore_header">備份與還原</string>
<string name="settings_about_version_copied_to_clipboard_message">已複製到剪貼簿</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="detail_settings_appearance_header">主題</string>
<string name="detail_settings_notifications_instant_title">即時通知</string>
<string name="detail_settings_global_setting_suffix">使用全域設定</string>
<string name="user_dialog_title_add">新增使用者</string>
<string name="main_menu_donate_title">捐獻 💸</string>
<string name="detail_item_snack_undo">復原</string>
<string name="detail_item_download_info_downloading_x_percent">已下載 %1$d%%</string>
<string name="detail_menu_enable_instant">啓用即時通知</string>
<string name="detail_menu_disable_instant">關閉即時通知</string>
<string name="detail_menu_clear">清除所有通知</string>
<string name="detail_action_mode_menu_copy">複製</string>
<string name="detail_action_mode_menu_delete">刪除</string>
<string name="share_menu_send">傳送</string>
<string name="share_content_title">預覽</string>
<string name="notification_dialog_cancel">取消</string>
<string name="notification_dialog_save">儲存</string>
<string name="notification_dialog_30min">30 分鐘</string>
<string name="notification_dialog_1h">1 小時</string>
<string name="notification_dialog_2h">2 小時</string>
<string name="notification_dialog_8h">8 小時</string>
<string name="notification_dialog_tomorrow">直到明天</string>
<string name="notification_dialog_forever">直到我解除為止</string>
<string name="notification_popup_action_open">開啓</string>
<string name="notification_popup_action_browse">瀏覽</string>
<string name="notification_popup_action_download">下載</string>
<string name="notification_popup_action_cancel">取消</string>
<string name="notification_popup_file">%1$s
\n檔案 %2$s</string>
<string name="notification_popup_file_downloading">下載中 %1$s, %2$d%%
\n%3$s</string>
<string name="notification_popup_file_download_successful">%1$s
\n檔案下載已完成 %2$s</string>
<string name="notification_popup_file_download_failed">%1$s
\n檔案下載失敗 %2$s</string>
<string name="notification_popup_user_action_failed">%1$s 失敗: %2$s</string>
<string name="settings_title">設定</string>
<string name="settings_notifications_muted_until_show_all">顯示所有通知</string>
<string name="settings_notifications_priority_min">分鐘</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_summary_smaller_than_x">自動下載小於 %1$s 的附件</string>
<string name="settings_notifications_auto_download_never">從不自動下載</string>
<string name="settings_notifications_auto_download_always">自動下載所有東西</string>
<string name="settings_notifications_auto_download_500k">小於 500 kB</string>
<string name="settings_notifications_auto_download_100k">小於 100 kB</string>
<string name="settings_notifications_auto_download_1m">小於 1 MB</string>
<string name="settings_notifications_auto_download_5m">小於 5 MB</string>
<string name="settings_notifications_auto_download_50m">小於 50 MB</string>
<string name="settings_notifications_auto_delete_title">自動刪除通知</string>
<string name="settings_notifications_auto_delete_summary_never">從不自動刪除通知</string>
<string name="settings_notifications_auto_delete_summary_one_day">自動刪除 1 天前的通知</string>
<string name="settings_notifications_auto_delete_summary_three_days">自動刪除 3 天前的通知</string>
<string name="settings_notifications_auto_delete_summary_one_week">自動刪除 1 週前的通知</string>
<string name="settings_notifications_auto_delete_summary_three_months">自動刪除 3 個月前的通知</string>
<string name="settings_notifications_auto_delete_never">從不</string>
<string name="settings_general_users_prefs_title">使用者</string>
<string name="settings_general_users_prefs_user_add">新增使用者</string>
<string name="settings_general_users_prefs_user_add_title">建立新使用者</string>
<string name="settings_general_dark_mode_title">黑暗模式</string>
<string name="settings_backup_restore_backup_entry_everything">全部</string>
<string name="settings_backup_restore_backup_entry_settings_only">僅備份設定</string>
<string name="settings_backup_restore_backup_entry_everything_no_users">全部備份 (使用者檔案除外)</string>
<string name="settings_backup_restore_backup_successful">已成功備份</string>
<string name="settings_backup_restore_restore_successful">還原成功</string>
<string name="settings_backup_restore_restore_failed">還原失敗: %1$s</string>
<string name="settings_advanced_header">進階</string>
<string name="settings_advanced_export_logs_scrub_dialog_button_ok">確定</string>
<string name="settings_advanced_connection_protocol_title">連接方式</string>
<string name="settings_advanced_connection_protocol_summary_jsonhttp">透過 HTTP 連接伺服器來接收 JSON 串流,但可能會持續消耗較大電量。</string>
<string name="settings_advanced_connection_protocol_summary_ws">(建議) 透過 WebSocket 連接伺服器,但伺服器可能需要額外設定來啓用這種連接方式。</string>
<string name="settings_advanced_connection_protocol_entry_jsonhttp">JSON 串流 (透過 HTTP 連接)</string>
<string name="settings_advanced_connection_protocol_entry_ws">WebSocket</string>
<string name="detail_settings_about_topic_url_title">主題 URL</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">已複製到剪貼簿</string>
<string name="user_dialog_password_hint_add">密碼</string>
<string name="user_dialog_button_save">儲存</string>
<string name="user_dialog_button_add">新增使用者</string>
<string name="user_dialog_button_cancel">取消</string>
<string name="user_dialog_username_hint">使用者名稱</string>
<string name="settings_notifications_header">通知</string>
<string name="detail_menu_test">發送測試訊息</string>
<string name="settings_notifications_muted_until_title">暫停通知</string>
<string name="settings_notifications_auto_download_10m">小於 10 MB</string>
<string name="detail_settings_about_header">關於</string>
<string name="settings_backup_restore_backup_failed">備份失敗: %1$s</string>
</resources> </resources>

View file

@ -154,6 +154,7 @@
<string name="detail_item_cannot_open">Cannot open attachment: %1$s</string> <string name="detail_item_cannot_open">Cannot open attachment: %1$s</string>
<string name="detail_item_cannot_open_not_found">Cannot open attachment: The file may have been deleted, or no installed app can open the file.</string> <string name="detail_item_cannot_open_not_found">Cannot open attachment: The file may have been deleted, or no installed app can open the file.</string>
<string name="detail_item_cannot_open_url">Cannot open URL: %1$s</string> <string name="detail_item_cannot_open_url">Cannot open URL: %1$s</string>
<string name="detail_item_cannot_open_apk">Apps cannot be installed anymore. Download via browser instead. See issue #531 for details.</string>
<string name="detail_item_cannot_save">Cannot save attachment: %1$s</string> <string name="detail_item_cannot_save">Cannot save attachment: %1$s</string>
<string name="detail_item_cannot_delete">Cannot delete attachment: %1$s</string> <string name="detail_item_cannot_delete">Cannot delete attachment: %1$s</string>
<string name="detail_item_download_failed">Could not download attachment: %1$s</string> <string name="detail_item_download_failed">Could not download attachment: %1$s</string>
@ -318,7 +319,7 @@
<string name="settings_advanced_broadcast_summary_disabled">Apps cannot receive notifications as broadcasts</string> <string name="settings_advanced_broadcast_summary_disabled">Apps cannot receive notifications as broadcasts</string>
<string name="settings_advanced_record_logs_title">Record logs</string> <string name="settings_advanced_record_logs_title">Record logs</string>
<string name="settings_advanced_record_logs_summary_enabled">Logging (up to 1,000 entries) to device …</string> <string name="settings_advanced_record_logs_summary_enabled">Logging (up to 1,000 entries) to device …</string>
<string name="settings_advanced_record_logs_summary_disabled">Turn on logging so you can share logs later to diagnose issues.</string> <string name="settings_advanced_record_logs_summary_disabled">Turn on logging, so you can share logs later to diagnose issues.</string>
<string name="settings_advanced_export_logs_title">Copy/upload logs</string> <string name="settings_advanced_export_logs_title">Copy/upload logs</string>
<string name="settings_advanced_export_logs_summary">Copy logs to the clipboard, or upload to nopaste.net (owned by the ntfy author). Hostnames and topics can be censored, notifications will never be.</string> <string name="settings_advanced_export_logs_summary">Copy logs to the clipboard, or upload to nopaste.net (owned by the ntfy author). Hostnames and topics can be censored, notifications will never be.</string>
<string name="settings_advanced_export_logs_entry_copy_original">Copy to clipboard</string> <string name="settings_advanced_export_logs_entry_copy_original">Copy to clipboard</string>

View file

@ -14,6 +14,7 @@ import io.heckel.ntfy.msg.ApiService
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
import io.heckel.ntfy.util.nullIfZero
import io.heckel.ntfy.util.toPriority import io.heckel.ntfy.util.toPriority
import io.heckel.ntfy.util.topicShortUrl import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.work.PollWorker import io.heckel.ntfy.work.PollWorker
@ -94,8 +95,8 @@ class FirebaseService : FirebaseMessagingService() {
val encoding = data["encoding"] val encoding = data["encoding"]
val attachmentName = data["attachment_name"] ?: "attachment.bin" val attachmentName = data["attachment_name"] ?: "attachment.bin"
val attachmentType = data["attachment_type"] val attachmentType = data["attachment_type"]
val attachmentSize = data["attachment_size"]?.toLongOrNull() val attachmentSize = data["attachment_size"]?.toLongOrNull()?.nullIfZero()
val attachmentExpires = data["attachment_expires"]?.toLongOrNull() val attachmentExpires = data["attachment_expires"]?.toLongOrNull()?.nullIfZero()
val attachmentUrl = data["attachment_url"] val attachmentUrl = data["attachment_url"]
val truncated = (data["truncated"] ?: "") == "1" val truncated = (data["truncated"] ?: "") == "1"
if (id == null || topic == null || message == null || timestamp == null) { if (id == null || topic == null || message == null || timestamp == null) {

View file

@ -12,6 +12,8 @@ Bug fixes + maintenance:
* Fix topics do not re-subscribe to Firebase after restoring from backup (#511) * Fix topics do not re-subscribe to Firebase after restoring from backup (#511)
* Fix crashes from large images (#474, thanks to @daedric7 for reporting) * Fix crashes from large images (#474, thanks to @daedric7 for reporting)
* Fix notification click opens wrong subscription (#261, thanks to @SMAW for reporting) * Fix notification click opens wrong subscription (#261, thanks to @SMAW for reporting)
* Fix Firebase-only "link expired" issue (#529)
* Remove "Install .apk" feature in Google Play variant due to policy change (#531)
* Add donate button (no ticket) * Add donate button (no ticket)
Additional translations: Additional translations:

View file

@ -0,0 +1,2 @@
Bug fixes:
* Android 5 (SDK 21): Fix crash on unsubscribing (#528, thanks to Roger M.)

View file

@ -0,0 +1,17 @@
不論 Bash 還是 PowerShell或者是你自己的應用程式、curl 或者 Invoke-WebRequest 等等,都可以透過 HTTP PUT/POST 向你的裝置傳送通知。
ntfy 是 https://ntfy.sh 的 Android APP一個打造在 HTTP 標準之上的免費開源 pub-sub 服務。你可以透過訂閱主題來接收通過 HTTP API 發送的通知。
當中,你可能找到千變萬化的用途,例如:
* 在一個很長很長的程序完成後通知自己
* 在備份失敗後通知自己
* 當有人登入到伺服器的時候發送通知
發送通知可以簡單如此:
$ curl -d "備份完成了!" ntfy.sh/mytopic
你也可以在下面的連結閱讀更多資訊:
* 網站: https://ntfy.sh
* GitHub伺服器端 https://github.com/binwiederhier/ntfy
* GitHubAndroid APP https://github.com/binwiederhier/ntfy-android

View file

@ -0,0 +1 @@
透過 HTTP PUT/POST 方式傳送任何通知到你的裝置

View file

@ -0,0 +1 @@
ntfy - PUT/POST 到你的裝置