diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6ca9d3c0..4305b77a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
+
@@ -131,14 +132,15 @@
android:enabled="true"
android:exported="false"/>
-
+
-
+
+
diff --git a/app/src/main/java/io/heckel/ntfy/app/Application.kt b/app/src/main/java/io/heckel/ntfy/app/Application.kt
index d594d670..da9219ea 100644
--- a/app/src/main/java/io/heckel/ntfy/app/Application.kt
+++ b/app/src/main/java/io/heckel/ntfy/app/Application.kt
@@ -1,8 +1,11 @@
package io.heckel.ntfy.app
import android.app.Application
+import android.net.ConnectivityManager
+import android.net.Network
import com.google.android.material.color.DynamicColors
import io.heckel.ntfy.db.Repository
+import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -24,5 +27,18 @@ class Application : Application() {
if (repository.getDynamicColorsEnabled()) {
DynamicColors.applyToActivitiesIfAvailable(this)
}
+ registerNetworkCallback()
+ }
+
+ private fun registerNetworkCallback() {
+ val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ SubscriberServiceManager.refresh(this@Application)
+ }
+ override fun onLost(network: Network) {
+ SubscriberServiceManager.refresh(this@Application)
+ }
+ })
}
}
diff --git a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
index 2dafa839..2bb28909 100644
--- a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
+++ b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
@@ -90,6 +90,9 @@ class Backuper(val context: Context) {
if (settings.mutedUntil != null) {
repository.setGlobalMutedUntil(settings.mutedUntil)
}
+ if (settings.connectionAlertSeconds != null) {
+ repository.setConnectionAlertSeconds(settings.connectionAlertSeconds)
+ }
if (settings.lastSharedTopics != null) {
settings.lastSharedTopics.forEach { repository.addLastShareTopic(it) }
}
@@ -278,6 +281,7 @@ class Backuper(val context: Context) {
recordLogs = repository.getRecordLogs(),
defaultBaseUrl = repository.getDefaultBaseUrl() ?: "",
mutedUntil = repository.getGlobalMutedUntil(),
+ connectionAlertSeconds = repository.getConnectionAlertSeconds(),
lastSharedTopics = repository.getLastShareTopics()
)
}
@@ -421,6 +425,7 @@ data class Settings(
val recordLogs: Boolean?,
val defaultBaseUrl: String?,
val mutedUntil: Long?,
+ val connectionAlertSeconds: Long?,
val lastSharedTopics: List?,
)
diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt
index b6622a12..3101ae2c 100644
--- a/app/src/main/java/io/heckel/ntfy/db/Database.kt
+++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt
@@ -575,6 +575,9 @@ interface SubscriptionDao {
""")
fun getLastNotificationId(subscriptionIds: Collection): String?
+ @Query("UPDATE subscription SET icon = :icon WHERE id = :subscriptionId")
+ fun updateSubscriptionIcon(subscriptionId: Long, icon: String?)
+
@Query("DELETE FROM subscription WHERE id = :subscriptionId")
fun remove(subscriptionId: Long)
}
diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt
index a6716c74..4de802d8 100644
--- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt
+++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt
@@ -92,6 +92,10 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
subscriptionDao.update(subscription)
}
+ fun updateSubscriptionIcon(subscriptionId: Long, icon: String?) {
+ subscriptionDao.updateSubscriptionIcon(subscriptionId, icon)
+ }
+
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun removeSubscription(subscription: Subscription) {
@@ -440,13 +444,24 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
}
}
- fun getConnectionAlertSnoozeUntil(): Long {
- return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL, CONNECTION_ALERT_SNOOZE_UNTIL_DEFAULT)
+ fun getConnectionAlertSeconds(): Long {
+ return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SECONDS, CONNECTION_ALERT_DEFAULT)
}
- fun setConnectionAlertSnoozeUntil(timeMillis: Long) {
+ fun setConnectionAlertSeconds(seconds: Long) {
sharedPrefs.edit {
- putLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL, timeMillis)
+ putLong(SHARED_PREFS_CONNECTION_ALERT_SECONDS, seconds)
+ putLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL_TIME, 0L)
+ }
+ }
+
+ fun getConnectionAlertSnoozeUntilTime(): Long {
+ return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL_TIME, 0L)
+ }
+
+ fun setConnectionAlertSnoozeUntilTime(timeMillis: Long) {
+ sharedPrefs.edit {
+ putLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL_TIME, timeMillis)
}
}
@@ -608,6 +623,11 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
return connectionDetails.toMap()
}
+ fun clearConnectionDetails() {
+ connectionDetails.clear()
+ connectionDetailsLiveData.postValue(emptyMap())
+ }
+
fun getConnectionForceReconnectVersion(baseUrl: String): Long {
return connectionForceReconnectVersions[baseUrl] ?: 0L
}
@@ -634,14 +654,13 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
const val SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED = "InsistentMaxPriority"
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
const val SHARED_PREFS_MESSAGE_BAR_ENABLED = "MessageBarEnabled"
- const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
- const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner)
- const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime"
+ const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime" // Timestamp as millis
+ const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner), timestamp as millis
+ const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime" // Timestamp as millis
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" // Legacy key required for migration to DefaultBaseURL
const val SHARED_PREFS_DEFAULT_BASE_URL = "DefaultBaseURL"
- const val SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL = "ConnectionAlertSnoozeUntil"
- const val CONNECTION_ALERT_SNOOZE_UNTIL_DEFAULT = 0L
- const val CONNECTION_ALERT_NEVER_SHOW = Long.MAX_VALUE
+ const val SHARED_PREFS_CONNECTION_ALERT_SECONDS = "ConnectionAlertSeconds"
+ const val SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL_TIME = "ConnectionAlertSnoozeUntilTime" // Timestamp in millis
const val SHARED_PREFS_LAST_TOPICS = "LastTopics"
private const val LAST_TOPICS_COUNT = 3
@@ -653,12 +672,14 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
const val MUTED_UNTIL_FOREVER = 1L
const val MUTED_UNTIL_TOMORROW = 2L
- private const val ONE_MB = 1024 * 1024L
+ private const val ONE_MB_BYTES = 1024 * 1024L
const val AUTO_DOWNLOAD_NEVER = 0L // Values must match values.xml
const val AUTO_DOWNLOAD_ALWAYS = 1L
- const val AUTO_DOWNLOAD_DEFAULT = ONE_MB
+ const val AUTO_DOWNLOAD_DEFAULT = ONE_MB_BYTES
+
+ private const val ONE_HOUR_SECONDS = 60 * 60L
+ private const val ONE_DAY_SECONDS = 24 * ONE_HOUR_SECONDS
- private const val ONE_DAY_SECONDS = 24 * 60 * 60L
const val AUTO_DELETE_USE_GLOBAL = -1L // Values must match values.xml
const val AUTO_DELETE_NEVER = 0L
const val AUTO_DELETE_ONE_DAY_SECONDS = ONE_DAY_SECONDS
@@ -671,6 +692,14 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
const val INSISTENT_MAX_PRIORITY_USE_GLOBAL = -1 // Values must match values.xml
const val INSISTENT_MAX_PRIORITY_ENABLED = 1 // 0 = Disabled (but not needed in code)
+ const val CONNECTION_ALERT_NEVER = 0L
+ const val CONNECTION_ALERT_FIVE_MINUTES_SECONDS = 5 * 60L
+ const val CONNECTION_ALERT_FIFTEEN_MINUTES_SECONDS = 15 * 60L
+ const val CONNECTION_ALERT_ONE_HOUR_SECONDS = ONE_HOUR_SECONDS
+ const val CONNECTION_ALERT_THREE_HOURS_SECONDS = 3 * ONE_HOUR_SECONDS
+ const val CONNECTION_ALERT_TWELVE_HOURS_SECONDS = 12 * ONE_HOUR_SECONDS
+ const val CONNECTION_ALERT_DEFAULT = CONNECTION_ALERT_NEVER
+
const val CONNECTION_PROTOCOL_JSONHTTP = "jsonhttp"
const val CONNECTION_PROTOCOL_WS = "ws"
diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
index 56d3233c..06b4a7c1 100644
--- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
@@ -24,12 +24,12 @@ import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
-import io.heckel.ntfy.msg.NotificationService
-import io.heckel.ntfy.util.PRIORITY_HIGH
import io.heckel.ntfy.ui.Colors
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.HttpUtil
import io.heckel.ntfy.util.Log
+import io.heckel.ntfy.util.isNetworkAvailable
+import io.heckel.ntfy.util.shortUrl
import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -316,22 +316,28 @@ class SubscriberService : Service() {
}
private fun maybeShowConnectionAlert() {
- val now = System.currentTimeMillis()
+ val thresholdSeconds = repository.getConnectionAlertSeconds()
+ if (thresholdSeconds <= 0L) return
- // Check snooze / never-show-again
- val snoozeUntil = repository.getConnectionAlertSnoozeUntil()
- if (snoozeUntil == Repository.CONNECTION_ALERT_NEVER_SHOW) return
+ // Don't show alert if the device has no network connectivity (e.g. airplane mode)
+ if (!isNetworkAvailable(this)) return
+
+ // Check snooze
+ val now = System.currentTimeMillis()
+ val snoozeUntil = repository.getConnectionAlertSnoozeUntilTime()
if (snoozeUntil > now) return
- // Check if any connection has been in error for 15+ minutes
+ // Check if any connection has been in error for longer than the threshold
+ val thresholdMillis = thresholdSeconds * 1000L
val allDetails = repository.getConnectionDetails()
val disconnectedUrls = allDetails.filter { (_, details) ->
details.hasError() && details.firstErrorTime > 0L &&
- (now - details.firstErrorTime) >= CONNECTION_ALERT_THRESHOLD_MILLIS
+ (now - details.firstErrorTime) >= thresholdMillis
}.keys
if (disconnectedUrls.isNotEmpty()) {
- showConnectionAlertNotification(disconnectedUrls)
+ val thresholdMinutes = (thresholdSeconds / 60).toInt()
+ showConnectionAlertNotification(disconnectedUrls, thresholdMinutes)
}
}
@@ -345,47 +351,40 @@ class SubscriberService : Service() {
}
}
- private fun showConnectionAlertNotification(disconnectedUrls: Set) {
+ private fun showConnectionAlertNotification(disconnectedUrls: Set, thresholdMinutes: Int) {
val text = if (disconnectedUrls.size == 1) {
- getString(R.string.connection_alert_text_one, disconnectedUrls.first(), CONNECTION_ALERT_THRESHOLD_MINUTES)
+ getString(R.string.connection_alert_text_one, shortUrl(disconnectedUrls.first()), thresholdMinutes)
} else {
- getString(R.string.connection_alert_text_multiple, disconnectedUrls.size, CONNECTION_ALERT_THRESHOLD_MINUTES)
+ getString(R.string.connection_alert_text_multiple, disconnectedUrls.size, thresholdMinutes)
}
- val dismissIntent = Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply {
- action = CONNECTION_ALERT_ACTION_DISMISS
- }
- val dismissPendingIntent = PendingIntent.getBroadcast(this, 0, dismissIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
-
- val snoozeIntent = Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply {
- action = CONNECTION_ALERT_ACTION_SNOOZE
- }
- val snoozePendingIntent = PendingIntent.getBroadcast(this, 1, snoozeIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
-
- val neverIntent = Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply {
- action = CONNECTION_ALERT_ACTION_NEVER
- }
- val neverPendingIntent = PendingIntent.getBroadcast(this, 2, neverIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
-
val contentIntent = PendingIntent.getActivity(this, 0,
Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
+ val snoozeShortIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_SNOOZE_SHORT },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val snoozeIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_SNOOZE_LONG },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val neverAlertIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_NEVER },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val deleteIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_DISMISS },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- val channelId = NotificationService(this).toChannelId(NotificationService.DEFAULT_GROUP, PRIORITY_HIGH)
- val notification = NotificationCompat.Builder(this, channelId)
+ val notification = NotificationCompat.Builder(this, NOTIFICATION_CONNECTION_ALERT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setColor(Colors.notificationIcon(this))
.setContentTitle(getString(R.string.connection_alert_title))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setContentIntent(contentIntent)
- .setAutoCancel(true)
.setOnlyAlertOnce(true)
- .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_dismiss), dismissPendingIntent).build())
- .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_snooze), snoozePendingIntent).build())
- .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_never), neverPendingIntent).build())
+ .setDeleteIntent(deleteIntent)
+ .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_snooze, CONNECTION_ALERT_SNOOZE_SHORT_HOURS), snoozeShortIntent).build())
+ .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_snooze, CONNECTION_ALERT_SNOOZE_LONG_HOURS), snoozeIntent).build())
+ .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_never), neverAlertIntent).build())
.build()
Log.d(TAG, "Showing connection alert notification")
@@ -440,6 +439,9 @@ class SubscriberService : Service() {
it
}
notificationManager.createNotificationChannel(channel)
+ val connectionAlertChannelName = getString(R.string.channel_connection_alert_name)
+ val connectionAlertChannel = NotificationChannel(NOTIFICATION_CONNECTION_ALERT_CHANNEL_ID, connectionAlertChannelName, NotificationManager.IMPORTANCE_DEFAULT)
+ notificationManager.createNotificationChannel(connectionAlertChannel)
return notificationManager
}
@@ -500,22 +502,19 @@ class SubscriberService : Service() {
Log.d(TAG, "ConnectionAlertBroadcastReceiver: action=${intent.action}")
val repository = Repository.getInstance(context)
val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
-
when (intent.action) {
- CONNECTION_ALERT_ACTION_DISMISS -> {
- notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID)
+ CONNECTION_ALERT_ACTION_DISMISS, CONNECTION_ALERT_ACTION_SNOOZE_SHORT -> {
+ repository.setConnectionAlertSnoozeUntilTime(System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_SHORT_MILLIS)
}
- CONNECTION_ALERT_ACTION_SNOOZE -> {
- repository.setConnectionAlertSnoozeUntil(
- System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_DURATION_MILLIS
- )
- notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID)
+ CONNECTION_ALERT_ACTION_SNOOZE_LONG -> {
+ repository.setConnectionAlertSnoozeUntilTime(System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_LONG_MILLIS)
}
CONNECTION_ALERT_ACTION_NEVER -> {
- repository.setConnectionAlertSnoozeUntil(Repository.CONNECTION_ALERT_NEVER_SHOW)
- notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID)
+ repository.setConnectionAlertSeconds(Repository.CONNECTION_ALERT_NEVER)
}
+ else -> return
}
+ notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID)
}
}
@@ -529,18 +528,24 @@ class SubscriberService : Service() {
const val SERVICE_START_WORKER_VERSION = BuildConfig.VERSION_CODE
const val SERVICE_START_WORKER_WORK_NAME_PERIODIC = "NtfyAutoRestartWorkerPeriodic" // Do not change!
+ private const val ONE_HOUR_SECONDS = 60 * 60L
+ private const val ONE_HOUR_MILLIS = ONE_HOUR_SECONDS * 1000L
+
private const val WAKE_LOCK_TAG = "SubscriberService:lock"
private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"
+ private const val NOTIFICATION_CONNECTION_ALERT_CHANNEL_ID = "ntfy-connection-alert"
private const val NOTIFICATION_GROUP_ID = "io.heckel.ntfy.NOTIFICATION_GROUP_SERVICE"
private const val NOTIFICATION_SERVICE_ID = 2586
private const val NOTIFICATION_RECEIVED_WAKELOCK_TIMEOUT_MILLIS = 10 * 60 * 1000L /*10 minutes*/
- private const val NOTIFICATION_CONNECTION_ALERT_ID = 2587
- private const val CONNECTION_ALERT_THRESHOLD_MINUTES = 15
- private const val CONNECTION_ALERT_THRESHOLD_MILLIS = CONNECTION_ALERT_THRESHOLD_MINUTES * 60 * 1000L
- private const val CONNECTION_ALERT_SNOOZE_DURATION_MILLIS = 60 * 60 * 1000L /*1 hour*/
+ const val NOTIFICATION_CONNECTION_ALERT_ID = 2587
+ private const val CONNECTION_ALERT_SNOOZE_SHORT_HOURS = 1
+ private const val CONNECTION_ALERT_SNOOZE_SHORT_MILLIS = CONNECTION_ALERT_SNOOZE_SHORT_HOURS * ONE_HOUR_MILLIS
+ private const val CONNECTION_ALERT_SNOOZE_LONG_HOURS = 8
+ private const val CONNECTION_ALERT_SNOOZE_LONG_MILLIS = CONNECTION_ALERT_SNOOZE_LONG_HOURS * ONE_HOUR_MILLIS
private const val CONNECTION_ALERT_ACTION_DISMISS = "io.heckel.ntfy.CONNECTION_ALERT_DISMISS"
- private const val CONNECTION_ALERT_ACTION_SNOOZE = "io.heckel.ntfy.CONNECTION_ALERT_SNOOZE"
+ private const val CONNECTION_ALERT_ACTION_SNOOZE_SHORT = "io.heckel.ntfy.CONNECTION_ALERT_SNOOZE_SHORT"
+ private const val CONNECTION_ALERT_ACTION_SNOOZE_LONG = "io.heckel.ntfy.CONNECTION_ALERT_SNOOZE_LONG"
private const val CONNECTION_ALERT_ACTION_NEVER = "io.heckel.ntfy.CONNECTION_ALERT_NEVER"
}
}
diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt
index 71f51b48..f577b10c 100644
--- a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt
@@ -1,11 +1,13 @@
package io.heckel.ntfy.service
+import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import androidx.work.*
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.util.Log
+import io.heckel.ntfy.util.isNetworkAvailable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -40,8 +42,9 @@ class SubscriberServiceManager(private val context: Context) {
withContext(Dispatchers.IO) {
val app = context.applicationContext as Application
val subscriptionIdsWithInstantStatus = app.repository.getSubscriptionIdsWithInstantStatus()
+ val hasNetwork = isNetworkAvailable(context)
val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size
- if (instantSubscriptions > 0) {
+ if (instantSubscriptions > 0 && hasNetwork) {
// We have instant subscriptions, start the service
Log.d(TAG, "ServiceStartWorker: Starting foreground service (work ID: ${id})")
Intent(context, SubscriberService::class.java).also {
@@ -56,9 +59,14 @@ class SubscriberServiceManager(private val context: Context) {
}
}
} else {
- // No instant subscriptions, stop the service using stopService()
+ // No instant subscriptions (or no network), stop the service using stopService()
// This avoids ForegroundServiceDidNotStartInTimeException, see #1520
Log.d(TAG, "ServiceStartWorker: Stopping service (work ID: ${id})")
+ if (!hasNetwork) {
+ app.repository.clearConnectionDetails()
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancel(SubscriberService.NOTIFICATION_CONNECTION_ALERT_ID)
+ }
Intent(context, SubscriberService::class.java).also {
context.stopService(it)
}
diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
index dd2187f8..00eff09c 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
@@ -120,7 +120,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
messageView.autoLinkMask = 0
markwon.setMarkdown(messageView, message.toString())
} else {
- messageView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS
+ messageView.autoLinkMask = Linkify.WEB_URLS
messageView.text = message
}
messageView.movementMethod = BetterLinkMovementMethod.getInstance()
diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt
index f0293c79..577c3675 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt
@@ -486,7 +486,7 @@ class DetailSettingsActivity : AppCompatActivity() {
}
private fun createUri(): Uri? {
- val dir = File(requireContext().cacheDir, SUBSCRIPTION_ICONS)
+ val dir = File(requireContext().filesDir, SUBSCRIPTION_ICONS)
if (!dir.exists() && !dir.mkdirs()) {
return null
}
@@ -526,7 +526,6 @@ class DetailSettingsActivity : AppCompatActivity() {
companion object {
private const val TAG = "NtfyDetailSettingsActiv"
- private const val SUBSCRIPTION_ICONS = "subscriptionIcons"
private const val SUBSCRIPTION_ICON_MAX_SIZE_BYTES = 4194304
private const val SUBSCRIPTION_ICON_MAX_WIDTH = 2048
private const val SUBSCRIPTION_ICON_MAX_HEIGHT = 2048
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
index 251a4ba0..6b6ff1fb 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -6,8 +6,11 @@ import android.animation.AnimatorListenerAdapter
import android.app.AlarmManager
import android.app.AlertDialog
import android.content.ActivityNotFoundException
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.Network
import android.os.Build
import android.os.Bundle
import android.provider.Settings
@@ -58,11 +61,13 @@ import io.heckel.ntfy.msg.Poller
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.Log
+import io.heckel.ntfy.util.SUBSCRIPTION_ICONS
import io.heckel.ntfy.util.dangerButton
import io.heckel.ntfy.util.displayName
import io.heckel.ntfy.util.formatDateShort
import io.heckel.ntfy.util.isDarkThemeOn
import io.heckel.ntfy.util.isIgnoringBatteryOptimizations
+import io.heckel.ntfy.util.isNetworkAvailable
import io.heckel.ntfy.util.maybeSplitTopicUrl
import io.heckel.ntfy.util.randomSubscriptionId
import io.heckel.ntfy.util.shortUrl
@@ -72,8 +77,10 @@ import io.heckel.ntfy.work.PollWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import java.io.File
import java.util.Date
import java.util.concurrent.TimeUnit
+import androidx.core.content.FileProvider
import androidx.core.view.size
import androidx.core.view.get
import androidx.core.net.toUri
@@ -95,6 +102,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
private lateinit var fab: FloatingActionButton
// Other stuff
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var workManager: WorkManager? = null // Context-dependent
private var dispatcher: NotificationDispatcher? = null // Context-dependent
private var appBaseUrl: String? = null // Context-dependent
@@ -342,6 +350,19 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
}
}
+ // Network state banner
+ showHideNoNetworkBanner()
+ val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ networkCallback = object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ runOnUiThread { showHideNoNetworkBanner() }
+ }
+ override fun onLost(network: Network) {
+ runOnUiThread { showHideNoNetworkBanner() }
+ }
+ }
+ connectivityManager.registerDefaultNetworkCallback(networkCallback!!)
+
// Hide links that lead to payments, see https://github.com/binwiederhier/ntfy/issues/1463
val howToLink = findViewById(R.id.main_how_to_link)
howToLink.isVisible = BuildConfig.PAYMENT_LINKS_AVAILABLE
@@ -362,6 +383,9 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
// Permissions
maybeRequestNotificationPermission()
+
+ // FIXME 2026-05-04: Remove this migration after 1 month
+ migrateSubscriptionIconsFromCache()
}
private fun maybeRequestNotificationPermission() {
@@ -377,6 +401,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
super.onResume()
showHideNotificationMenuItems()
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
+ showHideNoNetworkBanner()
redrawList()
}
@@ -425,6 +450,22 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
}
}
+ private fun showHideNoNetworkBanner() {
+ val banner = findViewById(R.id.main_banner_no_network)
+ banner.visibility = if (isNetworkAvailable(this)) View.GONE else View.VISIBLE
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ try {
+ val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
+ } catch (e: Exception) {
+ Log.d(TAG, "Failed to unregister network callback: ${e.message}")
+ }
+ networkCallback = null
+ }
+
private fun schedulePeriodicPollWorker() {
val workerVersion = repository.getPollWorkerVersion()
val workPolicy = if (workerVersion == PollWorker.VERSION) {
@@ -859,6 +900,38 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
adapter.notifyItemRangeChanged(0, adapter.currentList.size)
}
+ // FIXME 2026-05-04: Remove this migration after 1 month
+ private fun migrateSubscriptionIconsFromCache() {
+ lifecycleScope.launch(Dispatchers.IO) {
+ delay(5_000) // 5 seconds
+ try {
+ val oldDir = File(cacheDir, SUBSCRIPTION_ICONS)
+ if (!oldDir.exists() || !oldDir.isDirectory) return@launch
+ val newDir = File(filesDir, SUBSCRIPTION_ICONS)
+ if (!newDir.exists()) newDir.mkdirs()
+ oldDir.listFiles()?.forEach { oldFile ->
+ val newFile = File(newDir, oldFile.name)
+ if (newFile.exists()) {
+ oldFile.delete()
+ return@forEach
+ }
+ if (oldFile.renameTo(newFile)) {
+ val subscriptionId = oldFile.name.toLongOrNull() ?: return@forEach
+ val newUri = FileProvider.getUriForFile(
+ this@MainActivity,
+ BuildConfig.APPLICATION_ID + ".provider",
+ newFile
+ )
+ repository.updateSubscriptionIcon(subscriptionId, newUri.toString())
+ }
+ }
+ oldDir.delete()
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to migrate subscription icons", e)
+ }
+ }
+ }
+
companion object {
const val TAG = "NtfyMainActivity"
const val EXTRA_SUBSCRIPTION_ID = "subscriptionId"
diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt
index eaa8631e..77d5278f 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt
@@ -10,9 +10,7 @@ import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
-import android.text.TextUtils
import android.view.View
-import android.widget.Button
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
@@ -43,7 +41,6 @@ import kotlinx.coroutines.launch
import okhttp3.RequestBody.Companion.toRequestBody
import java.text.SimpleDateFormat
import java.util.*
-import java.util.concurrent.TimeUnit
/**
* Main settings
@@ -328,6 +325,31 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
}
}
+ // Connection alert
+ val connectionAlertPrefId = context?.getString(R.string.settings_advanced_connection_alert_key) ?: return
+ val connectionAlert: ListPreference? = findPreference(connectionAlertPrefId)
+ connectionAlert?.value = repository.getConnectionAlertSeconds().toString()
+ connectionAlert?.preferenceDataStore = object : PreferenceDataStore() {
+ override fun putString(key: String?, value: String?) {
+ val seconds = value?.toLongOrNull() ?:return
+ repository.setConnectionAlertSeconds(seconds)
+ }
+ override fun getString(key: String?, defValue: String?): String {
+ return repository.getConnectionAlertSeconds().toString()
+ }
+ }
+ connectionAlert?.summaryProvider = Preference.SummaryProvider { pref ->
+ when (pref.value.toLongOrNull() ?: repository.getConnectionAlertSeconds()) {
+ Repository.CONNECTION_ALERT_NEVER -> getString(R.string.settings_advanced_connection_alert_summary_never)
+ Repository.CONNECTION_ALERT_FIVE_MINUTES_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_five_minutes)
+ Repository.CONNECTION_ALERT_FIFTEEN_MINUTES_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_fifteen_minutes)
+ Repository.CONNECTION_ALERT_ONE_HOUR_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_one_hour)
+ Repository.CONNECTION_ALERT_THREE_HOURS_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_three_hours)
+ Repository.CONNECTION_ALERT_TWELVE_HOURS_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_twelve_hours)
+ else -> getString(R.string.settings_advanced_connection_alert_summary_never) // Must match default const
+ }
+ }
+
// Dark mode
val darkModePrefId = context?.getString(R.string.settings_general_dark_mode_key) ?: return
val darkMode: ListPreference? = findPreference(darkModePrefId)
diff --git a/app/src/main/java/io/heckel/ntfy/util/Constants.kt b/app/src/main/java/io/heckel/ntfy/util/Constants.kt
index d708011f..e2438962 100644
--- a/app/src/main/java/io/heckel/ntfy/util/Constants.kt
+++ b/app/src/main/java/io/heckel/ntfy/util/Constants.kt
@@ -1,6 +1,7 @@
package io.heckel.ntfy.util
const val ANDROID_APP_MIME_TYPE = "application/vnd.android.package-archive"
+const val SUBSCRIPTION_ICONS = "subscriptionIcons"
const val PRIORITY_MIN = 1
const val PRIORITY_LOW = 2
diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt
index e472fcef..3c857c8e 100644
--- a/app/src/main/java/io/heckel/ntfy/util/Util.kt
+++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt
@@ -10,6 +10,7 @@ import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.RippleDrawable
+import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
@@ -546,3 +547,10 @@ fun deriveNotificationId(baseUrl: String, topic: String, sequenceId: String): In
val hash = composite.hashCode()
return if (hash == 0 || hash == Int.MIN_VALUE) 1 else abs(hash)
}
+
+// We check for any active network, not specifically for internet connectivity,
+// because the ntfy server may be on a LAN without internet access.
+fun isNetworkAvailable(context: Context): Boolean {
+ val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ return connectivityManager.activeNetwork != null
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_wifi_off_gray_24dp.xml b/app/src/main/res/drawable/ic_wifi_off_gray_24dp.xml
new file mode 100644
index 00000000..9dbc7e1d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_wifi_off_gray_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 1bf20371..1642bb78 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -232,6 +232,28 @@
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/main_banner_no_network">
diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml
index 613c980d..0194a51d 100644
--- a/app/src/main/res/values-af/strings.xml
+++ b/app/src/main/res/values-af/strings.xml
@@ -33,4 +33,4 @@
Weergawe
WebSockets
Kopieer na knipbord (gesensuur)
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 25bed2e1..c6deeabd 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -471,4 +471,9 @@
Търсене в известията
Подразбирани настройки
Въведете адрес на услуга, напр. https://ntfy.example.com
+ Връзката е загубена
+ От най-малко %2$d минути няма връзка с %1$s
+ От най-малко %2$d минути няма връзка със сървърите на %1$d
+ Отлагане с %1$dч
+ Да не се показва повече
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 0f073361..63d05e74 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -103,7 +103,7 @@
Esborra URL de servei
Encara no has rebut cap notificació en aquest tema.
Per enviar notificacions en aquest tema, fes PUT o POST a la URL del tema.
- Exemple (usant curl):
$ curl -d \"Hola\" %1$s
+ Exemple (usant curl):
$ curl -d \"Hola\" %1$s
Aquesta subscripció és gestionada per %1$s via UnifiedPush
Instruccions detallades disponibles a ntfy.sh i a la documentació.
WebSockets és el mètode recomanat per connectar-te al teu servidor. Pot millorar el rendiment de la bateria, però pot requerir configuració addicional al teu proxy. Aquesta opció pot ser habil·litada a Configuració.
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 5067c2dc..d84d6988 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -4,7 +4,7 @@
Standard-Priorität
Hohe Priorität
Höchste Priorität
- Abo Service
+ Hintergrunddienst
Niedrigste Priorität
Warte auf eingehende Benachrichtigungen
Ein Sofortnachrichten-Thema abonniert
@@ -471,4 +471,24 @@
In Benachrichtigungen suchen
Eine gültige Service-URL eingeben, z. B. https://ntfy.example.com
Benachrichtigungen suchen
+ Verbindung verloren
+ Es konnte länger als %2$d Minuten lang keine Verbindung zu %1$s hergestellt werden. Prüfe deine Netzwerkverbindung.
+ Es konnte länger als %2$d Minuten lang keine Verbindung zu %1$d Servern hergestellt werden. Prüfe deine Netzwerkverbindung.
+ %1$d Std. schlummern
+ Niemals zeigen
+ Verbindungswarnungen
+ Du bist offline
+ Warnung, wenn die Verbindung getrennt wird
+ Niemals benachrichtigen, wenn der ntfy-Server nicht erreichbar ist
+ Benachrichtigen, wenn der ntfy-Server länger als 5 Minuten nicht erreichbar ist
+ Benachrichtigen, wenn der ntfy-Server länger als 15 Minuten nicht erreichbar ist
+ Benachrichtige, wenn der ntfy-Server länger als 1 Stunde nicht erreichbar ist
+ Benachrichtigen, wenn der ntfy-Server länger als 3 Stunden nicht erreichbar ist
+ Benachrichtigen, wenn der ntfy-Server länger als 12 Stunden nicht erreichbar ist
+ Niemals
+ Nach 5 Minuten
+ Nach 15 Minuten
+ Nach 1 Stunde
+ Nach 3 Stunden
+ Nach 12 Stunden
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 51e8d907..0df8ca8a 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -80,4 +80,4 @@
Αυτό το θέμα απαιτεί να συνδεθείτε. Παρακαλώ πληκτρολογήστε ένα όνομα χρήστη και έναν κωδικό πρόσβασης.
Απορρίψτε το
Πίσω
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 8df653cc..4961a23e 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -141,8 +141,8 @@
Descargar automáticamente archivos adjuntos de hasta %1$s
Nunca descargar nada automáticamente
Siempre descargar todo automáticamente
- Si es más pequeño que 100 KB
- Si es más pequeño que 500 KB
+ Si es inferior a 100 kB
+ Si es inferior a 500 kB
Si es más pequeño que 5 MB
Eliminar notificaciones
Nunca eliminar automáticamente las notificaciones
@@ -342,11 +342,11 @@
Descartar
Conceder ahora
Alarmas exactas
- ntfy puede programar alarmas exactas (utilizadas para reconectar WebSockets en segundo plano)
- ntfy no puede programar alarmas exactas. Haga clic para conceder el permiso ahora
+ ntfy puede programar alarmas exactas. Las alarmas exactas son requeridas para reconectar WebSockets en segundo plano. Haga clic para revocar este permiso.
+ ntfy no puede programar alarmas exactas. Las alarmas exactas son requeridas para reconectar WebSockets en segundo plano. Haga clic para conceder el permiso.
Publicar a %1$s
Título
- p.e. Alguien está en la puerta
+ ej. Alguien está en la puerta
Mensaje
Etiquetas
Prioridad
@@ -407,6 +407,73 @@
Añadir certificados a la lista de confiados y administrar certificados de cliente para mTLS
Certificados confiados
ej. https://ntfy.example.com
- Subject
+ Asunto
Error de conexión
+ Restaurar valores por defecto
+ Conexión perdida
+ No se pudo conectar a %1$s por más de %2$d minutos
+ No se pudo conectar a %1$d servidores por más de %2$d minutos
+ Suspender %1$dh
+ Nunca mostrar
+ Su búsqueda no tuvo ningún resultado
+ Buscar notificaciones
+ Buscar en notificaciones
+ ej. alerta, peligro
+ Email
+ Markdown
+ Email
+ ej. phil@example.com
+ ej. 30m, 1h, today 9pm (Siempre en inglés)
+ ej. https://example.com/flowers.jpg
+ ej. ilies.jpg
+ ej. +1234567890
+ Error de conexión
+ Hubo un problema al conectar a %1$s. La aplicación continuará intentando reconectar en segundo plano.
+ Conexión rechazada. Puede que el servidor esté desconectado o la dirección sea incorrecta.
+ WebSocket no soportado. Puede que el servidor no soporte conexiones WebSocket, o que la dirección sea incorrecta.
+ No autorizado. El servidor devolvió una respuesta HTTP 401/403. Por favor, compruebe su usuario y contraseña.
+ Reintentar ahora
+ Reintentando en %1$ds…
+ Reintentando…
+ Certificado fijado, emitido por %1$s, expira %2$s, usado para conexiones a %3$s
+ Certificado fijado, emitido por %1$s, expirado, usado para conexiones a %2$s
+ Añadir un certificado de confianza
+ Importa un certificado en el almacén de confianza (PEM). Al conectarse al servidor ntfy, se confiará en este certificado.
+ Certificados de cliente (mTLS)
+ Certificado de cliente, emitido por %1$s, expira %2$s, usado para conectar a %3$s
+ Certificado de cliente, emitido por %1$s, expirado, usado para conectar a %2$s
+ Añadir un certificado de cliente
+ Importar certificado para autenticación TLS mutua (PKCS#12). Este certificado se utilizará al conectarse al servidor.
+ Archivo de certificado no válido
+ Archivo PKCS#12 inválido
+ No se puede añadir el usuario si se ha configurado una cabecera Authorization personalizada para este servidor
+ Introduce una URL del servicio válida, ej. (https://ntfy.example.com)
+ Añadir encabezado personalizado
+ Editar encabezado personalizado
+ Nombre de la cabecera (ej. CF-Access-Client-Id)
+ Valor de la cabecera (ej. 9f3c2e4a1b2d4e)
+ Añade una cabecera HTTP personalizada que se enviará con cada solicitud al servidor especificado.
+ Puedes editar el nombre o el valor de la cabecera seleccionada, o eliminarla.
+ El nombre de la cabecera contiene caracteres no válidos
+ Detalles del certificado
+ Advertencia de seguridad
+ Añadir certificado de confianza
+ Tu conexión no es privada
+ El certificado del servidor no es de confianza. Es posible que atacantes estén intentando robar tu información. No continúes a menos que sepas por qué este certificado no es de confianza.
+ Has seleccionado un archivo de certificado. Revisa los detalles a continuación antes de añadirlo a tus certificados de confianza.
+ Este certificado se utiliza para las conexiones a la URL del servicio indicada abajo. Has añadido esta excepción manualmente.
+ Introduce la URL del servicio a la que debe fijarse este certificado. El certificado solo será de confianza para esta URL.
+ Advertencia: Este certificado ha caducado.
+ Advertencia: Este certificado aún no es válido.
+ URL no válida
+ No se puede cargar el certificado: %1$s
+ Confiar
+ Certificado de cliente
+ Añadir certificado de cliente
+ Introduce la URL del servicio para la que debe usarse este certificado y la contraseña del archivo PKCS#12.
+ Revisa los detalles del certificado y guarda para añadir este certificado de cliente. Este certificado se utilizará para autenticarse con el servidor.
+ Contraseña
+ Contraseña incorrecta o archivo PKCS#12 no válido
+ Contraseña no válida o archivo PKCS#12 dañado
+ URL del servicio no válida
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index e46f2ad4..c0639a4a 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -461,4 +461,9 @@
Otsi teavitustest
Taasta algväärtused
Sisesta korrektne teenuse võrguaadress, nt https://ntfy.toredomeen.com
+ Ühendus on katkenud
+ Ühendus %1$s teenusega toimib vaid %2$d minuti(t)
+ Ühendus %1$d serveriga toimib vaid %2$d minuti(t)
+ Tukasta %1$dt
+ Ära näita iialgi
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index c57b708c..ac32bd0f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -347,10 +347,10 @@
Couleurs dynamiques
Utiliser les couleurs de thème ntfy
Utiliser les couleurs dynamiques du système
- e.g. Quelqu\'un est à la porte
+ par ex. Quelqu\'un est à la porte
Message
Étiquettes
- e.g. warning, skull
+ par ex. attention, crâne
Priorité
Publier
Impossible de publier le message : %1$s
@@ -390,7 +390,7 @@
Afficher la barre de message
Barre de message affichée en bas du sujet
Bouton Publier affiché en bas du sujet
- En-têtes HTTP personnalisés
+ En-têtes personnalisés
Définissez des en-têtes HTTP personnalisés qui seront envoyés avec chaque demande, par exemple, si votre serveur ntfy se trouve derrière un proxy ou un tunnel authentifié.
Ajouter un en-tête
Ajouter un en-tête pour un serveur
@@ -471,4 +471,9 @@
Rechercher dans les notifications
Réinitialiser par défaut
Entrez une URL de service valide, par ex. https://ntfy.example.com
+ Connexion perdue
+ Impossible de se connecter à %1$s depuis plus de %2$d minutes
+ Impossible de se connecter à %1$d serveurs depuis plus de %2$d minutes
+ Sourdine %1$dh
+ Ne jamais montrer
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index b31ab142..7ee933b8 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -85,7 +85,7 @@
विस्तृत निर्देश ntfy.sh पर और डॉक्स में उपलब्ध हैं।
स्थायी रूप से हटाएं
इस विषय पर सूचनाएं भेजने के लिए, बस विषय URL पर PUT या POST करें।
- उदाहरण (curl का उपयोग करके):
$curl -d \"नमस्कार\"%1$s
+ उदाहरण (curl का उपयोग करके):
$curl -d \"नमस्कार\"%1$s
इस विषय की सभी सूचनाएँ हटाएँ?
WebSockets पर स्विच करना आपके सर्वर से कनेक्ट करने का अनुशंसित तरीका है, और बैटरी जीवन में सुधार कर सकता है, लेकिन आपके proxy में अतिरिक्त कॉन्फ़िगरेशन की आवश्यकता हो सकती है। इसे सेटिंग्स में टॉगल किया जा सकता है।
संदेश नहीं भेजा जा सकता: अनाम प्रकाशन की अनुमति नहीं है।
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 818e76e2..83a77754 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -83,4 +83,4 @@
Prijavu grešku
Opširnije upute dostupne su na ntfy.sh i u dokumentaciji.
Klikni + za kreiranje ili pretplaćivanje na temu. Nakon toga primaš obavijesti na uređaj kad pošalješ poruke preko PUT ili POST.
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index c2a22474..a1c4aeae 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -471,4 +471,9 @@
通知内容を検索します
既定にリセット
有効なサービスURLを入力してください、例: https://ntfy.example.com
+ 接続が切断されました
+ %2$d 分以上 %1$s に接続できませんでした
+ %1$d 個のサーバーに %2$d 分以上接続できませんでした
+ %1$d時間後に再通知
+ 再表示しない
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index 994adff5..a9a646e9 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -61,7 +61,7 @@
Forsikrer at meldinger blir levert umiddelbart, selv når enheten er inaktiv.
Dette emnet krever at du logger inn. Skriv inn et brukernavn og passord.
For å sende merknader til dette emnet kan du utføre PUT eller POST til emne-nettadressen.
- Eksempel (ved bruk av curl):
$ curl -d \"Hei\" %1$s
+ Eksempel (ved bruk av curl):
$ curl -d \"Hei\" %1$s
Merknad slettet
Avbryt nedlasting
Lagre fil
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 27e6ed8e..eb1ca62e 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -364,4 +364,64 @@
Fout bij publiceren: %1$s
Kon bericht niet publiceren: %1$s (code %2$d)
Bericht gepubliceerd
+ Terug naar standaardinstellingen
+ Geldig vanaf
+ Uitgever
+ SHA-256 vingerafdruk
+ Onderwerp
+ Onjuiste server URL
+ Vul de server URL in waar dit certificaat voor moet worden gebruikt, en het wachtwoord voor het PKCS#12 bestand.
+ Beoordeel de certificaat instellingen en sla op om het certificaat toe te voegen. Dit certificaat zal worden gebruikt om te authenticeren met de server.
+ Wachtwoord
+ Onjuist wachtwoord of onjuist PKCS#12 bestand
+ Onjuist wachtwoord of beschadigd PKCS#12 bestand
+ Verbinding verbroken
+ Kan geen verbinding maken met %1$s voor meer dan %2$d minuten
+ Kan geen verbinding maken met %1$d servers voor meer dan %2$d minuten
+ Sluimer voor %1$d uur
+ Nooit laten zien
+ Publieer naar %1$s
+ Uploaden: %1$s (%2$s / %3$s)
+ Upload geannuleerd
+ Titel
+ Labels
+ Prioriteit
+ Klik URL
+ Email
+ Vertraging
+ Markdown
+ Insluiten via URL
+ Bestand insluiten
+ Telefoongesprek
+ Klik URL
+ bijvoorbeeld https://example.com/alerts/1234
+ Email
+ bijvoorbeeld jan@jansen.nl
+ Vertraging bezorging
+ Attachment URL
+ bijvoorbeeld https://example.com/flowers.jpg
+ Attachment bestandsnaam
+ bijvoorbeeld lilies.jpg
+ Telefoongesprek
+ bijvoorbeel +1234567890
+ Bekijk de documentatie voor voorbeelden en een uitgebreide beschrijving van alle publiceer-functies.
+ Berichtenbalk
+ Publiceer bericht
+ Meer opties
+ Fout bij verbinden
+ Er was een probleem bij het verbinden met %1$s. De app zal blijven proberen in de achtergrond.
+ Verbinding geweigerd. De server is mogelijk niet bereikbaar of het adres is onjuist.
+ WebSocket niet ondersteund. De server ondersteund mogelijk geen WebSochet, of het adres is onjuist.
+ Niet toegestaan. De server gaf een HTTP 401/403 antwoord. Controleer uw gebruikersnaam en/of wachtwoord.
+ Probeer opnieuw
+ Nieuwe poging in %1$d…
+ Opnieuw proberen…
+ Taal
+ Systeemstandaarden worden gebruikt
+ Systeemstandaarden
+ Dynamische kleuren
+ Dynamische systeemkleuren worden gebruikt
+ Ntfy thema kleuren worden gebruikt
+ Toon berichtenbalk
+ Berichtenbalk getoond onderaan onderwerpweergave
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index cae5a5c0..f8bc7a9d 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -341,4 +341,135 @@
Alarmes exatos
O ntfy pode agendar alarmes exatos, eles são usados para reconectar WebSockets em segundo plano. Clique para revogar a permissão.
O ntfy não pode agendar alarmes exatos, eles são usados para reconectar WebSockets em segundo plano. Clique para conceder a permissão.
+ Próximo
+ Adicionar
+ Apagar
+ Restaurar Padrões
+ ex. https://ntfy.example.com
+ Dono
+ Emissor
+ Assinatura SHA-256
+ Válido de
+ Válido até
+ Certificado adicionado
+ Certificado removido
+ Notificação de alerta de conexão
+ Não foi possível conectar à %1$s por mais de %2$d minutos
+ Não foi possível conectar aos servidores %1$d por mais de %2$d minutos
+ Adiar %1$dh
+ Nunca mostrar
+ Erro de conexão
+ Certificado do servidor não confiável
+ Sua busca não retornou resultados
+ Notificações de pesquisa
+ Buscar em notificações
+ Publicar em %1$s
+ Título
+ ex. Tem alguém na porta
+ Mensagem
+ Tags
+ ex. Alerta, skull
+ Prioridade
+ Publicar
+ Não foi possível publicar a mensagem: %1$s
+ Não foi possível publicar a mensagem: %1$s (código %2$d)
+ Mensagem publicada
+ Subindo: %1$s (%2$s / %3$s)
+ Upload cancelado
+ Título
+ Etiquetas
+ Prioridade
+ Click URL
+ Email
+ Atraso
+ Markdown
+ Anexar com URL
+ Anexar arquivo local
+ Chamada telefônica
+ Click URL
+ ex. https://example.com/alerts/1234
+ Email
+ ex. phil@example.com
+ Atraso na entrega
+ URL do anexo
+ ex. https://example.com/flowers.jpg
+ Nome de arquivo do anexo
+ ex. lilies.jpg
+ Chamada telefônica
+ ex. +1234567890
+ Para exemplos e uma descrição detalhada de todos os recursos de publicação, por favor consulte documentation.
+ Barra de mensagens
+ Publicar mensagem
+ Mais opções
+ Confie
+ Certificado cliente
+ Adicionar certificado cliente
+ Insira a URL de serviço para o qual este certificado deve ser usado e a senha para o arquivo PKCS#12.
+ Revisar os detalhes do certificado e salvar para adicionar este certificado de cliente. Este certificado será usado para autenticar com o servidor.
+ Senha
+ Senha incorreta ou arquivo PKCS#12 inválido
+ Senha inválida ou arquivo PKCS#12 corrompido
+ URL de serviço inválida
+ ex: 30m, 1h, today 9pm (expressões de tempo apenas em inglês)
+ Publicar notificação
+ Falha na conexão
+ Houve um problema ao conectar em %1$s. O app vai continuar tentando reconectar em segundo plano.
+ Conexão recusada. O servidor pode estar desligado ou o endereço pode estar incorreto.
+ WebSocket não suportado. O servidor pode não suportar conexões WebSocket ou o endereço pode estar incorreto.
+ Não autorizado. O servidor retornou uma resposta HTTP 401/403. Favor verificar se seu usuário e senha estão corretos.
+ Tentar de novo agora
+ Tentando de novo em %1$d…
+ Tentando de novo…
+ Idioma
+ Usando padrão do sistema
+ Padrão do sistema
+ Cores dinâmicas
+ Usando as cores dinâmicas do sistema
+ Usando as cores do tema ntfy
+ Mostrar barra de mensagens
+ Barra de mensagens mostrada abaixo da visualização de tópico
+ Botão publicar mostrado abaixo da visualização de tópico
+ Cabeçalhos customizados
+ Definir cabeçalhos HTTP personalizados que serão enviados junto com cada requisição, por ex. se seu servidor ntfy está atrás de um túnel ou proxy autenticado.
+ Adicionar cabeçalho
+ Adicionar um cabeçalho para um servidor
+ Cabeçalhos são incluídos em cada requisição HTTP. Cada servidor ntfy pode ter seu próprio conjunto de cabeçalhos personalizados.
+ Gerenciar certificados
+ Adicionar certificados para a trust store e gerenciar certificados de cliente para mTLS
+ Certificados confiáveis
+ Certificado fixado, emitido por %1$s,expira em %2$s, usado para conexões em %3$s
+ Certificado fixado, emitido por %1$s, expiradom usado para conexões em %2$s
+ Adicionar um certificado confiável
+ Importar um certificado para a trust store (PEM). Ao conectar ao servidor ntfy, este certificado será considerado confiável.
+ Certificado de cliente (mTLS)
+ Certificado de cliente, emitido por %1$s, expira em %2$s, usado para conexões com %3$s
+ Certificado de cliente, emitido por %1$s, expirado, usado para conexões com%2$s
+ Adicionar um certificado de cliente
+ Importar certificado para autenticação TLS mútua (PKCS#12). Este certificado será usado ao conectar com o servidor.
+ Arquivo de certificado inválido
+ Arquivo PKCS#12 inválido
+ Não é possível adicionar usuário se o cabeçalho de autorização personalizada está configurado para este servidor
+ Insira uma URL de serviço válida, por ex. https://ntfy.example.com
+ Adicionar cabeçalho personalizado
+ Editar cabeçalho personalizado
+ Nome do cabeçalho (ex. CF-Access-Client-Id)
+ Valor do cabeçalho (ex. 9f3c2e4a1b2d4e)
+ Adicionar um cabeçalho HTTP personalizado que será enviado com cada requisição para o servidor especificado.
+ Você pode editar o nome/valor do cabeçalho para o cabeçalho selecionado, ou apaga-lo.
+ O nome do cabeçalho contém caracteres inválidos
+ Este cabeçalho está reservado e configurado pelo ntfy
+ Não é possível adicionar um cabeçalho de autorização se um usuário está configurado para este servidor
+ Um cabeçalho com este nome já existe para este servidor
+ Detalhes do certificado
+ Alerta de segurança
+ Adicionar certificado confiável
+ Sua conexão não é privada
+ O certificado do servidor não é confiável. Atacantes podem estar tentando roubar suas informações. Não prossiga a menos que você saiba porque este certificado não é confiável.
+ Você selecionou um arquivo de certificado. Revise os detalhes abaixo antes de adiciona-lo aos seus certificados confiáveis.
+ Este certificado é usado para conexões à URL de serviço abaixo. Você adicionou esta exceção manualmente.
+ Insira a URL de serviço para o qual este certificado deveria ser fixado. O certificado será considerado confiável apenas para esta URL.
+ Alerta: Este certificado expirou.
+ Alerta: Este certificado ainda não é válido.
+ URL inválida
+ Não foi possível carregar certificado: %1$s
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 64dad04a..b3b6fb93 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -345,4 +345,26 @@
Alarmes exactos
O ntfy pode agendar alarmes exatos. Alarmes exatos são necessários para reconectar os WebSockets em segundo plano. Clique para revogar a permissão.
O ntfy não pode agendar alarmes exatos. Alarmes exatos são necessários para reconectar os WebSockets em segundo plano. Clique para conceder a permissão.
+ Próximo
+ Adicionar
+ Apagar
+ Restaurar valores padrão
+ ex. https://ntfy.example.com
+ Assunto
+ Emissor
+ Impressão digital SHA-256
+ Válido desde
+ Válido até
+ Certificado adicionado
+ Certificado apagado
+ Conexão perdida
+ ex. Alguém está à porta
+ Mensagem
+ Etiquetas
+ ex. aviso, perigo
+ Prioridade
+ Publicar
+ Não foi possível publicar a mensagem: %1$s
+ Não foi possível publicar a mensagem: %1$s (código %2$d)
+ Mensagem publicada
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index ec8b43f6..7b23b0bd 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -343,4 +343,7 @@
Alarme exacte
ntfy poate programa alarme exacte. Alarmele exacte sunt necesare pentru a reconecta WebSocket-urile în fundal. Faceți clic pentru a revoca permisiunea.
ntfy nu poate programa alarme exacte. Alarmele exacte sunt necesare pentru a reconecta WebSockets în fundal. Faceți clic pentru a acorda permisiunea.
+ Următorul
+ Adaugă
+ Şterge
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 860bdf41..ec604f07 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -132,9 +132,7 @@
Восстановить из файла
Добавить нового пользователя
Уведомления отключены до %1$s
- Не получилось обновить %1$d подписок
-\n
-\n%2$s
+ Не получилось обновить %1$d подписок\n\n%2$s
Не получилось обновить подписку: %1$s
Темы подписок
Нажмите \"+\", чтобы создать или подписаться на тему. Вы будете получать уведомления на вашем устройстве при отправке сообщений через PUT или POST-запросы.
@@ -466,4 +464,29 @@
Неверный пароль или недопустимый файл PKCS#12
Неверный пароль или поврежденный файл PKCS#12
Недопустимый URL сервиса
+ Сбросить по умолчанию
+ Не показывать
+ Соединение разорвано
+ Отложить на %1$dч
+ Невозможно подключиться к %1$s более %2$d мин. Проверьте подключение к сети.
+ Невозможно подключиться к %1$d серверам более %2$d мин. Проверьте подключение к сети.
+ Уведомления о подключении
+ Вы не в сети
+ Ничего не найдено
+ Искать в уведомлениях
+ Оповещение при отключении
+ Никогда не уведомлять о недоступности сервера
+ Уведомлять, если сервер недоступен более 5 минут
+ Уведомлять, если сервер недоступен более 15 минут
+ Уведомлять, если сервер недоступен более 1 часа
+ Уведомлять, если сервер недоступен более 3 часов
+ Уведомлять, если сервер недоступен более 12 часов
+ Никогда
+ Через 5 минут
+ Через 15 минут
+ Через 1 час
+ Через 3 часа
+ Через 12 часов
+ Укажите верный URL, например https://ntfy.example.com
+ Поиск уведомлений
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index cb1597ee..8c270ec9 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -51,7 +51,7 @@
தள்ளுபடி
இப்போது சரிசெய்யவும்
தெளிவான பணி முகவரி
- எடுத்துக்காட்டு (சுருட்டைப் பயன்படுத்துதல்):
$ சுருட்டை -d \"hi\" %1$s
+ எடுத்துக்காட்டு (சுருட்டைப் பயன்படுத்துதல்):
$ சுருட்டை -d \"hi\" %1$s
Ntfy.sh, மற்றும் டாக்சில் விரிவான வழிமுறைகள் கிடைக்கின்றன.
உடனடி வழங்கல்
தலைப்பு %1$s க்கு குழுசேர்ந்தது
diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml
index b3109426..e4d21931 100644
--- a/app/src/main/res/values-uz/strings.xml
+++ b/app/src/main/res/values-uz/strings.xml
@@ -71,7 +71,7 @@
URL manzilini tanlang
URL manzilini tozalang
Ushbu mavzu bo‘yicha bildirishnomalarni yuborish uchun, mavzu URL manziliga PUT yoki POST so‘rov yuboring.
- Misol (curl yordamida):
$ curl -d \"Salom\" %1$s
+ Misol (curl yordamida):
$ curl -d \"Salom\" %1$s
Batafsil ko‘rsatmalar ntfy.sh saytida va hujjatlarda mavjud.
Ushbu mavzudagi barcha bildirishnomalar o‘chirilsinmi?
Butunlay o‘chirish
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index c1d90d31..4fe9ca67 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -142,7 +142,7 @@
默认优先级
高优先级
最高优先级
- 订阅服务
+ 后台服务
正在监听通知
已订阅主题
已订阅 1 个主题
@@ -471,4 +471,24 @@
在通知中搜索
重置为默认
输入有效的服务 URL,如 https://ntfy.example.com
+ 连接丢失
+ 已无法连接到 %1$s 超过 %2$d 分钟。检查网络连接。
+ 已无法连接到 %1$d 台服务器超过 %2$d 分钟。检查网络连接。
+ 延后 %1$d 小时
+ 永不显示
+ 连接警报
+ 未联网
+ 连接断开时示警
+ 无法连接到 ntfy 服务器时永远不通知
+ 如果无法连接到 ntfy 服务器超过 5 分钟就发出通知
+ 如果无法连接到 ntfy 服务器超过 15 分钟就发出通知
+ 如果无法连接到 ntfy 服务器超过 1 小时就发出通知
+ 如果无法连接到 ntfy 服务器超过 3 小时就发出通知
+ 如果无法连接到 ntfy 服务器超过 12 小时就发出通知
+ 永不
+ 5 分钟后
+ 15 分钟后
+ 1 小时后
+ 3 小时后
+ 12 小时后
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index b272e0b7..a5553469 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -471,4 +471,9 @@
通知中的搜尋
重設為預設值
請輸入有效的服務 URL,例如:https://ntfy.example.com
+ 連線已中斷
+ 超過 %2$d 分鐘無法連線至 %1$s
+ 超過 %2$d 分鐘無法連線至 %1$d 個伺服器
+ 延後 %1$d 小時
+ 永不顯示
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 986a33e4..5979fb69 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -28,7 +28,8 @@
Default
- Subscription Service
+ Background service
+ Connection alerts
Listening for incoming notifications
Subscribed to instant delivery topics
Subscribed to one instant delivery topic
@@ -49,10 +50,10 @@
Connection lost
- Unable to connect to %1$s for more than %2$d minutes
- Unable to connect to %1$d servers for more than %2$d minutes
- Dismiss
- Snooze 1h
+ Unable to connect to %1$s for more than %2$d minutes. Check your network connection.
+ Unable to connect to %1$d servers for more than %2$d minutes. Check your network connection.
+
+ Snooze %1$dh
Never show
@@ -115,6 +116,7 @@
Ask later
Dismiss
Grant now
+ You\'re offline
Subscribe to topic
@@ -415,6 +417,19 @@
Restore successful
Restore failed: %1$s
Advanced
+ Alert when disconnected
+ Never notify when the ntfy server cannot be reached
+ Notify if the ntfy server cannot be reached for more than 5 minutes
+ Notify if the ntfy server cannot be reached for more than 15 minutes
+ Notify if the ntfy server cannot be reached for more than 1 hour
+ Notify if the ntfy server cannot be reached for more than 3 hours
+ Notify if the ntfy server cannot be reached for more than 12 hours
+ Never
+ After 5 minutes
+ After 15 minutes
+ After 1 hour
+ After 3 hours
+ After 12 hours
Broadcast messages
Apps can receive incoming notifications as broadcasts
Apps cannot receive notifications as broadcasts
diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml
index 0b45baf5..0bb4bca9 100644
--- a/app/src/main/res/values/values.xml
+++ b/app/src/main/res/values/values.xml
@@ -47,6 +47,7 @@
SubscriptionMinPriority
SubscriptionAutoDelete
SubscriptionInsistentMaxPriority
+ ConnectionAlert
SubscriptionAppearance
SubscriptionIconSet
SubscriptionIconRemove
@@ -167,6 +168,22 @@
- 1
- 0
+
+ - @string/settings_advanced_connection_alert_never
+ - @string/settings_advanced_connection_alert_five_minutes
+ - @string/settings_advanced_connection_alert_fifteen_minutes
+ - @string/settings_advanced_connection_alert_one_hour
+ - @string/settings_advanced_connection_alert_three_hours
+ - @string/settings_advanced_connection_alert_twelve_hours
+
+
+ - 0
+ - 300
+ - 900
+ - 3600
+ - 10800
+ - 43200
+
- @string/settings_advanced_connection_protocol_entry_jsonhttp
- @string/settings_advanced_connection_protocol_entry_ws
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
index a5b6b209..a120c699 100644
--- a/app/src/main/res/xml/file_paths.xml
+++ b/app/src/main/res/xml/file_paths.xml
@@ -2,4 +2,5 @@
+
diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml
index 1ef47d3e..c738b6c3 100644
--- a/app/src/main/res/xml/main_preferences.xml
+++ b/app/src/main/res/xml/main_preferences.xml
@@ -77,6 +77,12 @@
app:summary="@string/settings_backup_restore_restore_summary"/>
+