diff --git a/app/build.gradle b/app/build.gradle index a2d4e1a0..0526456b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { minSdkVersion 21 targetSdkVersion 35 - versionCode 41 - versionName "1.17.8" + versionCode 48 + versionName "1.19.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -53,10 +53,12 @@ android { play { buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true' + buildConfigField 'boolean', 'PAYMENT_LINKS_AVAILABLE', 'false' // Google Play Payments Policy, see #1463 } fdroid { buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false' buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false' + buildConfigField 'boolean', 'PAYMENT_LINKS_AVAILABLE', 'true' } } @@ -112,7 +114,7 @@ dependencies { implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' // Material design - implementation "com.google.android.material:material:1.9.0" + implementation "com.google.android.material:material:1.13.0" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1a11b210..46b5eab3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -87,6 +87,15 @@ android:exported="false"> + + + + + + + + + + if (menuItem.itemId == R.id.add_dialog_action_button) { + onActionButtonClick() + true + } else { + false + } + } + actionMenuItem = toolbar.menu.findItem(R.id.add_dialog_action_button) + // Main "pages" subscribeView = view.findViewById(R.id.add_dialog_subscribe_view) subscribeView.visibility = View.VISIBLE @@ -136,6 +153,19 @@ class AddFragment : DialogFragment() { } } + // Subscribe view validation + val subscribeTextWatcher = AfterChangedTextWatcher { + validateInputSubscribeView() + } + subscribeTopicText.addTextChangedListener(subscribeTextWatcher) + subscribeBaseUrlText.addTextChangedListener(subscribeTextWatcher) + subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, _ -> + validateInputSubscribeView() + } + subscribeUseAnotherServerCheckbox.setOnCheckedChangeListener { _, _ -> + validateInputSubscribeView() + } + // Username/password validation on type val loginTextWatcher = AfterChangedTextWatcher { validateInputLoginView() @@ -144,51 +174,36 @@ class AddFragment : DialogFragment() { loginPasswordText.addTextChangedListener(loginTextWatcher) // Build dialog - val dialog = AlertDialog.Builder(activity) - .setView(view) - .setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ -> - // This will be overridden below to avoid closing the dialog immediately - } - .setNegativeButton(R.string.add_dialog_button_cancel) { _, _ -> - // This will be overridden below - } - .create() + val dialog = Dialog(requireContext(), R.style.Theme_App_FullScreenDialog) + dialog.setContentView(view) - // Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785) - dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) - - // Add logic to disable "Subscribe" button on invalid input - dialog.setOnShowListener { - positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) - positiveButton.isEnabled = false - positiveButton.setOnClickListener { - positiveButtonClick() - } - negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE) - negativeButton.setOnClickListener { - negativeButtonClick() - } - val subscribeTextWatcher = AfterChangedTextWatcher { - validateInputSubscribeView() - } - subscribeTopicText.addTextChangedListener(subscribeTextWatcher) - subscribeBaseUrlText.addTextChangedListener(subscribeTextWatcher) - subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, _ -> - validateInputSubscribeView() - } - subscribeUseAnotherServerCheckbox.setOnCheckedChangeListener { _, _ -> - validateInputSubscribeView() - } - validateInputSubscribeView() - - // Focus topic text (keyboard is shown too, see above) - subscribeTopicText.requestFocus() - } + // Initial validation + validateInputSubscribeView() return dialog } - private fun positiveButtonClick() { + override fun onStart() { + super.onStart() + dialog?.window?.apply { + setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + } + + override fun onResume() { + super.onResume() + // Show keyboard after the dialog is fully visible + subscribeTopicText.postDelayed({ + subscribeTopicText.requestFocus() + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(subscribeTopicText, InputMethodManager.SHOW_FORCED) + }, 200) + } + + private fun onActionButtonClick() { val topic = subscribeTopicText.text.toString() val baseUrl = getBaseUrl() if (subscribeView.visibility == View.VISIBLE) { @@ -280,16 +295,8 @@ class AddFragment : DialogFragment() { } } - private fun negativeButtonClick() { - if (subscribeView.visibility == View.VISIBLE) { - dialog?.cancel() - } else if (loginView.visibility == View.VISIBLE) { - showSubscribeView() - } - } - private fun validateInputSubscribeView() { - if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play + if (!this::actionMenuItem.isInitialized) return // As per crash seen in Google Play // Show/hide things: This logic is intentionally kept simple. Do not simplify "just because it's pretty". val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) { @@ -327,11 +334,11 @@ class AddFragment : DialogFragment() { activity?.let { it.runOnUiThread { if (subscription != null || DISALLOWED_TOPICS.contains(topic)) { - positiveButton.isEnabled = false + actionMenuItem.isEnabled = false } else if (subscribeUseAnotherServerCheckbox.isChecked) { - positiveButton.isEnabled = validTopic(topic) && validUrl(baseUrl) + actionMenuItem.isEnabled = validTopic(topic) && validUrl(baseUrl) } else { - positiveButton.isEnabled = validTopic(topic) + actionMenuItem.isEnabled = validTopic(topic) } } } @@ -339,13 +346,13 @@ class AddFragment : DialogFragment() { } private fun validateInputLoginView() { - if (!this::positiveButton.isInitialized || !this::loginUsernameText.isInitialized || !this::loginPasswordText.isInitialized) { + if (!this::actionMenuItem.isInitialized || !this::loginUsernameText.isInitialized || !this::loginPasswordText.isInitialized) { return // As per crash seen in Google Play } if (loginUsernameText.visibility == View.GONE) { - positiveButton.isEnabled = true + actionMenuItem.isEnabled = true } else { - positiveButton.isEnabled = (loginUsernameText.text?.isNotEmpty() ?: false) + actionMenuItem.isEnabled = (loginUsernameText.text?.isNotEmpty() ?: false) && (loginPasswordText.text?.isNotEmpty() ?: false) } } @@ -372,8 +379,11 @@ class AddFragment : DialogFragment() { private fun showSubscribeView() { resetSubscribeView() - positiveButton.text = getString(R.string.add_dialog_button_subscribe) - negativeButton.text = getString(R.string.add_dialog_button_cancel) + toolbar.setTitle(R.string.add_dialog_title) + actionMenuItem.setTitle(R.string.add_dialog_button_subscribe) + toolbar.setNavigationOnClickListener { + dismiss() + } loginView.visibility = View.GONE subscribeView.visibility = View.VISIBLE if (subscribeTopicText.requestFocus()) { @@ -385,8 +395,11 @@ class AddFragment : DialogFragment() { private fun showLoginView(activity: Activity) { resetLoginView() loginProgress.visibility = View.INVISIBLE - positiveButton.text = getString(R.string.add_dialog_button_login) - negativeButton.text = getString(R.string.add_dialog_button_back) + toolbar.setTitle(R.string.add_dialog_login_title) + actionMenuItem.setTitle(R.string.add_dialog_button_login) + toolbar.setNavigationOnClickListener { + showSubscribeView() + } subscribeView.visibility = View.GONE loginView.visibility = View.VISIBLE if (loginUsernameText.requestFocus()) { @@ -400,7 +413,7 @@ class AddFragment : DialogFragment() { subscribeBaseUrlText.isEnabled = enable subscribeInstantDeliveryCheckbox.isEnabled = enable subscribeUseAnotherServerCheckbox.isEnabled = enable - positiveButton.isEnabled = enable + actionMenuItem.isEnabled = enable } private fun resetSubscribeView() { @@ -413,7 +426,7 @@ class AddFragment : DialogFragment() { private fun enableLoginView(enable: Boolean) { loginUsernameText.isEnabled = enable loginPasswordText.isEnabled = enable - positiveButton.isEnabled = enable + actionMenuItem.isEnabled = enable if (enable && loginUsernameText.requestFocus()) { val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT) diff --git a/app/src/main/java/io/heckel/ntfy/ui/BasePreferenceFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/BasePreferenceFragment.kt new file mode 100644 index 00000000..730827f5 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/ui/BasePreferenceFragment.kt @@ -0,0 +1,62 @@ +package io.heckel.ntfy.ui + +import android.widget.TextView +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import io.heckel.ntfy.R + +abstract class BasePreferenceFragment : PreferenceFragmentCompat() { + /** + * Show [ListPreference] and [EditTextPreference] dialog by [MaterialAlertDialogBuilder] + */ + override fun onDisplayPreferenceDialog(preference: Preference) { + when (preference) { + is ListPreference -> { + val prefIndex = preference.entryValues.indexOf(preference.value) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(preference.title) + .setSingleChoiceItems(preference.entries, prefIndex) { dialog, index -> + val newValue = preference.entryValues[index].toString() + if (preference.callChangeListener(newValue)) { + preference.value = newValue + } + dialog.dismiss() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + is EditTextPreference -> { + val view = layoutInflater.inflate(R.layout.preference_dialog_edittext_edited, null) + var message = "" + var hint = "" + if (preference.extras.getString("message") != null) { + message = preference.extras.getString("message")!! + } + if (preference.extras.getString("hint") != null) { + hint = preference.extras.getString("hint")!! + } + val messageView = view.findViewById(android.R.id.message) + messageView.text = message + val editText = view.findViewById(android.R.id.edit) + editText.setText(preference.text.toString()) + editText.hint = hint + MaterialAlertDialogBuilder(requireContext()) + .setTitle(preference.title) + .setView(view) + .setPositiveButton(android.R.string.ok) { _, _ -> + val newValue = editText.text.toString() + if (preference.callChangeListener(newValue)) { + preference.text = newValue + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + else -> super.onDisplayPreferenceDialog(preference) + } + } +} diff --git a/app/src/main/java/io/heckel/ntfy/ui/Colors.kt b/app/src/main/java/io/heckel/ntfy/ui/Colors.kt index 47e8e164..8363360e 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/Colors.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/Colors.kt @@ -1,48 +1,93 @@ package io.heckel.ntfy.ui import android.content.Context +import android.graphics.Color +import android.os.Build import androidx.core.content.ContextCompat +import com.google.android.material.color.MaterialColors import io.heckel.ntfy.R import io.heckel.ntfy.util.isDarkThemeOn class Colors { companion object { - val refreshProgressIndicator = R.color.teal + fun primary(context: Context): Int { + return MaterialColors.getColor(context, R.attr.colorPrimary, Color.GREEN) + } + + fun onPrimary(context: Context): Int { + return MaterialColors.getColor(context, R.attr.colorOnPrimary, Color.GREEN) + } fun notificationIcon(context: Context): Int { - return if (isDarkThemeOn(context)) R.color.teal_light else R.color.teal + return MaterialColors.getColor(context, R.attr.colorPrimary, Color.GREEN) + } + + fun linkColor(context: Context): Int { + return MaterialColors.getColor(context, R.attr.colorPrimary, Color.GREEN) } fun itemSelectedBackground(context: Context): Int { - return if (isDarkThemeOn(context)) R.color.black_800b else R.color.gray_400 - } - - fun cardBackground(context: Context): Int { - return if (isDarkThemeOn(context)) R.color.black_800b else R.color.white - } - - fun cardSelectedBackground(context: Context): Int { - return if (isDarkThemeOn(context)) R.color.black_700b else R.color.gray_500 + return ContextCompat.getColor(context, R.color.md_theme_surfaceContainerHigh) } fun cardBackgroundColor(context: Context): Int { - return ContextCompat.getColor(context, cardBackground(context)) + return if (isDarkThemeOn(context)) { + MaterialColors.getColor(context, R.attr.colorSurfaceContainer, Color.GRAY) + } else { + MaterialColors.getColor(context, R.attr.colorSurface, Color.WHITE) + } } fun cardSelectedBackgroundColor(context: Context): Int { - return ContextCompat.getColor(context, cardSelectedBackground(context)) + return if (isDarkThemeOn(context)) { + MaterialColors.getColor(context, R.attr.colorSurfaceContainerHigh, Color.GRAY) + } else { + MaterialColors.getColor(context, R.attr.colorSurfaceContainerHighest, Color.GRAY) + } } - fun statusBarNormal(context: Context): Int { - return if (isDarkThemeOn(context)) R.color.black_900 else R.color.teal + fun statusBarNormal(context: Context, dynamicColors: Boolean, darkMode: Boolean): Int { + val default = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + context.resources.getColor(R.color.action_bar, null) + } else { + @Suppress("DEPRECATION") + context.resources.getColor(R.color.action_bar) + } + return if (dynamicColors) { + // Use colorSurface for both light and dark mode when dynamic colors are enabled + MaterialColors.getColor(context, R.attr.colorSurface, default) + } else { + default + } } - fun statusBarActionMode(context: Context): Int { - return if (isDarkThemeOn(context)) R.color.black_900 else R.color.teal_dark + fun shouldUseLightStatusBar(dynamicColors: Boolean, darkMode: Boolean): Boolean { + // Use light status bar (dark icons) when dynamic colors are enabled in light mode + return dynamicColors && !darkMode + } + + fun toolbarTextColor(context: Context, dynamicColors: Boolean, darkMode: Boolean): Int { + return if (dynamicColors) { + // Use colorOnSurface (dark on light, light on dark) when dynamic colors are enabled + MaterialColors.getColor(context, R.attr.colorOnSurface, Color.BLACK) + } else { + if (darkMode) { + // In dark mode, toolbar is gray (surfaceContainer), so use light text + MaterialColors.getColor(context, R.attr.colorOnSurface, Color.WHITE) + } else { + // In light mode, toolbar is teal (primary), so use white text + MaterialColors.getColor(context, R.attr.colorOnPrimary, Color.WHITE) + } + } } fun dangerText(context: Context): Int { - return if (isDarkThemeOn(context)) R.color.red_light else R.color.red_dark + return MaterialColors.getColor(context, R.attr.colorError, Color.RED) + } + + fun swipeToRefreshColor(context: Context): Int { + return MaterialColors.getColor(context, R.attr.colorPrimary, Color.GREEN) } } } + diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index bf27fcb9..9655de35 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -10,7 +10,6 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.text.Html -import android.view.ActionMode import android.view.Menu import android.view.MenuItem import android.view.View @@ -18,11 +17,15 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode import androidx.core.content.ContextCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R @@ -31,17 +34,30 @@ import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.firebase.FirebaseMessenger -import io.heckel.ntfy.util.Log import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.service.SubscriberServiceManager -import io.heckel.ntfy.util.* -import kotlinx.coroutines.* -import java.util.* +import io.heckel.ntfy.util.Log +import io.heckel.ntfy.util.copyToClipboard +import io.heckel.ntfy.util.dangerButton +import io.heckel.ntfy.util.decodeMessage +import io.heckel.ntfy.util.displayName +import io.heckel.ntfy.util.formatDateShort +import io.heckel.ntfy.util.isDarkThemeOn +import io.heckel.ntfy.util.randomSubscriptionId +import io.heckel.ntfy.util.topicShortUrl +import io.heckel.ntfy.util.topicUrl +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.Date import kotlin.random.Random +import androidx.core.view.size +import androidx.core.view.get - -class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFragment.NotificationSettingsListener { +class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSettingsListener { private val viewModel by viewModels { DetailViewModelFactory((application as Application).repository) } @@ -67,6 +83,36 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra // Action mode stuff private var actionMode: ActionMode? = null + private val actionModeCallback = object : ActionMode.Callback { + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + actionMode = mode + if (mode != null) { + mode.menuInflater.inflate(R.menu.menu_detail_action_mode, menu) + mode.title = "1" // One item selected + } + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.detail_action_mode_copy -> { + onMultiCopyClick() + true + } + R.id.detail_action_mode_delete -> { + onMultiDeleteClick() + true + } + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode?) { + endActionModeAndRedraw() + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -78,9 +124,47 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra notifier = NotificationService(this) appBaseUrl = getString(R.string.app_base_url) + val toolbarLayout = findViewById(R.id.app_bar_drawer) + val dynamicColors = repository.getDynamicColorsEnabled() + val darkMode = isDarkThemeOn(this) + val statusBarColor = Colors.statusBarNormal(this, dynamicColors, darkMode) + val toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode) + toolbarLayout.setBackgroundColor(statusBarColor) + + val toolbar = toolbarLayout.findViewById(R.id.toolbar) + toolbar.setTitleTextColor(toolbarTextColor) + toolbar.setNavigationIconTint(toolbarTextColor) + toolbar.overflowIcon?.setTint(toolbarTextColor) + setSupportActionBar(toolbar) + + // Set system status bar color and appearance + window.statusBarColor = statusBarColor + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = + Colors.shouldUseLightStatusBar(dynamicColors, darkMode) + + // Set detail activity background: use theme background for dynamic colors, static gray for non-dynamic + val detailContentLayout = findViewById(R.id.detail_content_layout) + if (repository.getDynamicColorsEnabled()) { + detailContentLayout.setBackgroundColor( + com.google.android.material.color.MaterialColors.getColor( + this, + android.R.attr.colorBackground, + ContextCompat.getColor(this, R.color.detail_activity_background) + ) + ) + } else { + detailContentLayout.setBackgroundColor( + ContextCompat.getColor(this, R.color.detail_activity_background) + ) + } + // Show 'Back' button supportActionBar?.setDisplayHomeAsUpEnabled(true) + // Hide links that lead to payments, see https://github.com/binwiederhier/ntfy/issues/1463 + val howToLink = findViewById(R.id.detail_how_to_link) + howToLink.isVisible = BuildConfig.PAYMENT_LINKS_AVAILABLE + // Handle direct deep links to topic "ntfy://..." val url = intent?.data if (intent?.action == ACTION_VIEW && url != null) { @@ -152,7 +236,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic) - intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(subscription)) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(appBaseUrl, subscription)) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil) @@ -190,7 +274,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra // Swipe to refresh mainListContainer = findViewById(R.id.detail_notification_list_container) mainListContainer.setOnRefreshListener { refresh() } - mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator) + mainListContainer.setColorSchemeColors(Colors.swipeToRefreshColor(this)) // Update main list based on viewModel (& its datasource/livedata) val noEntriesText: View = findViewById(R.id.detail_no_notifications) @@ -277,10 +361,11 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra val subscription = repository.getSubscription(subscriptionId) ?: return@launch subscriptionInstant = subscription.instant subscriptionMutedUntil = subscription.mutedUntil - subscriptionDisplayName = displayName(subscription) + subscriptionDisplayName = displayName(appBaseUrl, subscription) showHideInstantMenuItems(subscriptionInstant) showHideMutedUntilMenuItems(subscriptionMutedUntil) + showHideCopyMenuItems(subscription.baseUrl) updateTitle(subscriptionDisplayName) } } @@ -312,10 +397,17 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_detail_action_bar, menu) this.menu = menu + + // Tint menu icons based on theme + val toolbarTextColor = Colors.toolbarTextColor(this, repository.getDynamicColorsEnabled(), isDarkThemeOn(this)) + for (i in 0 until menu.size) { + menu[i].icon?.setTint(toolbarTextColor) + } // Show and hide buttons showHideInstantMenuItems(subscriptionInstant) showHideMutedUntilMenuItems(subscriptionMutedUntil) + showHideCopyMenuItems(subscriptionBaseUrl) // Regularly check if "notification muted" time has passed // NOTE: This is done here, because then we know that we've initialized the menu items. @@ -559,6 +651,18 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra } } + + private fun showHideCopyMenuItems(subscriptionBaseUrl: String) { + if (!this::menu.isInitialized) { + return + } + runOnUiThread { + // Hide links that lead to payments, see https://github.com/binwiederhier/ntfy/issues/1463 + val copyUrlItem = menu.findItem(R.id.detail_menu_copy_url) + copyUrlItem?.isVisible = appBaseUrl != subscriptionBaseUrl || BuildConfig.PAYMENT_LINKS_AVAILABLE + } + } + private fun updateTitle(subscriptionDisplayName: String) { runOnUiThread { title = subscriptionDisplayName @@ -568,8 +672,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra private fun onClearClick() { Log.d(TAG, "Clearing all notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}") - val builder = AlertDialog.Builder(this) - val dialog = builder + val dialog = MaterialAlertDialogBuilder(this) .setMessage(R.string.detail_clear_dialog_message) .setPositiveButton(R.string.detail_clear_dialog_permanently_delete) { _, _ -> lifecycleScope.launch(Dispatchers.IO) { @@ -600,8 +703,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra private fun onDeleteClick() { Log.d(TAG, "Deleting subscription ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}") - val builder = AlertDialog.Builder(this) - val dialog = builder + val dialog = MaterialAlertDialogBuilder(this) .setMessage(R.string.detail_delete_dialog_message) .setPositiveButton(R.string.detail_delete_dialog_permanently_delete) { _, _ -> Log.d(TAG, "Deleting subscription with subscription ID $subscriptionId (topic: $subscriptionTopic)") @@ -664,33 +766,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra } } - override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { - this.actionMode = mode - if (mode != null) { - mode.menuInflater.inflate(R.menu.menu_detail_action_mode, menu) - mode.title = "1" // One item selected - } - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { - return false - } - - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { - return when (item?.itemId) { - R.id.detail_action_mode_copy -> { - onMultiCopyClick() - true - } - R.id.detail_action_mode_delete -> { - onMultiDeleteClick() - true - } - else -> false - } - } - private fun onMultiCopyClick() { Log.d(TAG, "Copying multiple notifications to clipboard") @@ -716,8 +791,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra private fun onMultiDeleteClick() { Log.d(TAG, "Showing multi-delete dialog for selected items") - val builder = AlertDialog.Builder(this) - val dialog = builder + val dialog = MaterialAlertDialogBuilder(this) .setMessage(R.string.detail_action_mode_delete_dialog_message) .setPositiveButton(R.string.detail_action_mode_delete_dialog_permanently_delete) { _, _ -> adapter.selected.map { notificationId -> viewModel.markAsDeleted(notificationId) } @@ -735,18 +809,9 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra dialog.show() } - override fun onDestroyActionMode(mode: ActionMode?) { - endActionModeAndRedraw() - } - private fun beginActionMode(notification: Notification) { - actionMode = startActionMode(this) + actionMode = startSupportActionMode(actionModeCallback) adapter.toggleSelection(notification.id) - - // Fade status bar color - val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this)) - val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this)) - fadeStatusBarColor(window, fromColor, toColor) } private fun finishActionMode() { @@ -758,11 +823,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra actionMode = null adapter.selected.clear() adapter.notifyItemRangeChanged(0, adapter.currentList.size) - - // Fade status bar color - val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this)) - val toColor = ContextCompat.getColor(this, Colors.statusBarNormal(this)) - fadeStatusBarColor(window, fromColor, toColor) } companion object { 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 ccc0191d..e012ba04 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt @@ -15,9 +15,11 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import androidx.preference.* import androidx.preference.Preference.OnPreferenceClickListener +import com.google.android.material.appbar.AppBarLayout import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R import io.heckel.ntfy.db.Repository @@ -63,6 +65,24 @@ class DetailSettingsActivity : AppCompatActivity() { .commit() } + val toolbarLayout = findViewById(R.id.app_bar_drawer) + val dynamicColors = repository.getDynamicColorsEnabled() + val darkMode = isDarkThemeOn(this) + val statusBarColor = Colors.statusBarNormal(this, dynamicColors, darkMode) + val toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode) + toolbarLayout.setBackgroundColor(statusBarColor) + + val toolbar = toolbarLayout.findViewById(R.id.toolbar) + toolbar.setTitleTextColor(toolbarTextColor) + toolbar.setNavigationIconTint(toolbarTextColor) + toolbar.overflowIcon?.setTint(toolbarTextColor) + setSupportActionBar(toolbar) + + // Set system status bar color and appearance + window.statusBarColor = statusBarColor + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = + Colors.shouldUseLightStatusBar(dynamicColors, darkMode) + // Title val displayName = intent.getStringExtra(DetailActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME) ?: return title = displayName @@ -87,6 +107,7 @@ class DetailSettingsActivity : AppCompatActivity() { private lateinit var openChannelsPref: Preference private lateinit var iconSetLauncher: ActivityResultLauncher private lateinit var iconRemovePref: Preference + private lateinit var appBaseUrl: String override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.detail_preferences, rootKey) @@ -96,6 +117,7 @@ class DetailSettingsActivity : AppCompatActivity() { serviceManager = SubscriberServiceManager(requireActivity()) notificationService = NotificationService(requireActivity()) resolver = requireContext().applicationContext.contentResolver + appBaseUrl = requireContext().getString(R.string.app_base_url) // Create result launcher for custom icon (must be created in onCreatePreferences() directly) iconSetLauncher = createIconPickLauncher() @@ -137,7 +159,7 @@ class DetailSettingsActivity : AppCompatActivity() { private fun loadInstantPref() { val appBaseUrl = getString(R.string.app_base_url) val prefId = context?.getString(R.string.detail_settings_notifications_instant_key) ?: return - val pref: SwitchPreference? = findPreference(prefId) + val pref: SwitchPreferenceCompat? = findPreference(prefId) pref?.isVisible = BuildConfig.FIREBASE_AVAILABLE && subscription.baseUrl == appBaseUrl pref?.isChecked = subscription.instant pref?.preferenceDataStore = object : PreferenceDataStore() { @@ -148,7 +170,7 @@ class DetailSettingsActivity : AppCompatActivity() { return subscription.instant } } - pref?.summaryProvider = Preference.SummaryProvider { preference -> + pref?.summaryProvider = Preference.SummaryProvider { preference -> if (preference.isChecked) { getString(R.string.detail_settings_notifications_instant_summary_on) } else { @@ -159,7 +181,7 @@ class DetailSettingsActivity : AppCompatActivity() { private fun loadDedicatedChannelsPrefs() { val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return - val pref: SwitchPreference? = findPreference(prefId) + val pref: SwitchPreferenceCompat? = findPreference(prefId) pref?.isVisible = true pref?.isChecked = subscription.dedicatedChannels pref?.preferenceDataStore = object : PreferenceDataStore() { @@ -176,7 +198,7 @@ class DetailSettingsActivity : AppCompatActivity() { return subscription.dedicatedChannels } } - pref?.summaryProvider = Preference.SummaryProvider { preference -> + pref?.summaryProvider = Preference.SummaryProvider { preference -> if (preference.isChecked) { getString(R.string.detail_settings_notifications_dedicated_channels_summary_on) } else { @@ -381,7 +403,7 @@ class DetailSettingsActivity : AppCompatActivity() { save(newSubscription) // Update activity title activity?.runOnUiThread { - activity?.title = displayName(newSubscription) + activity?.title = displayName(appBaseUrl, newSubscription) } // Update dedicated notification channel if (newSubscription.dedicatedChannels) { @@ -394,9 +416,10 @@ class DetailSettingsActivity : AppCompatActivity() { } pref?.summaryProvider = Preference.SummaryProvider { provider -> if (TextUtils.isEmpty(provider.text)) { + val appBaseUrl = context?.getString(R.string.app_base_url) getString( R.string.detail_settings_appearance_display_name_default_summary, - displayName(subscription) + displayName(appBaseUrl, subscription) ) } else { provider.text 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 14ffa80a..6d12f424 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -14,7 +14,6 @@ import android.os.Bundle import android.provider.Settings import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM import android.text.method.LinkMovementMethod -import android.view.ActionMode import android.view.Menu import android.view.MenuItem import android.view.View @@ -24,12 +23,25 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.view.ActionMode import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.text.HtmlCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import androidx.work.* +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.FloatingActionButton import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R @@ -43,17 +55,28 @@ import io.heckel.ntfy.msg.DownloadType import io.heckel.ntfy.msg.NotificationDispatcher import io.heckel.ntfy.service.SubscriberService import io.heckel.ntfy.service.SubscriberServiceManager -import io.heckel.ntfy.util.* +import io.heckel.ntfy.util.Log +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.maybeSplitTopicUrl +import io.heckel.ntfy.util.randomSubscriptionId +import io.heckel.ntfy.util.shortUrl +import io.heckel.ntfy.util.topicShortUrl import io.heckel.ntfy.work.DeleteWorker import io.heckel.ntfy.work.PollWorker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.util.* +import java.util.Date import java.util.concurrent.TimeUnit import kotlin.random.Random +import androidx.core.view.size +import androidx.core.view.get -class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.SubscribeListener, NotificationFragment.NotificationSettingsListener { +class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, NotificationFragment.NotificationSettingsListener { private val viewModel by viewModels { SubscriptionsViewModelFactory((application as Application).repository) } @@ -69,11 +92,39 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc private lateinit var fab: FloatingActionButton // Other stuff - private var actionMode: ActionMode? = null private var workManager: WorkManager? = null // Context-dependent private var dispatcher: NotificationDispatcher? = null // Context-dependent private var appBaseUrl: String? = null // Context-dependent + // Action mode stuff + private var actionMode: ActionMode? = null + private val actionModeCallback = object : ActionMode.Callback { + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + actionMode = mode + if (mode != null) { + mode.menuInflater.inflate(R.menu.menu_main_action_mode, menu) + mode.title = "1" // One item selected + } + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.main_action_mode_delete -> { + onMultiDeleteClick() + true + } + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode?) { + endActionModeAndRedraw() + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -87,18 +138,44 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc appBaseUrl = getString(R.string.app_base_url) // Action bar + val toolbarLayout = findViewById(R.id.app_bar_drawer) + val dynamicColors = repository.getDynamicColorsEnabled() + val darkMode = isDarkThemeOn(this) + val statusBarColor = Colors.statusBarNormal(this, dynamicColors, darkMode) + val toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode) + toolbarLayout.setBackgroundColor(statusBarColor) + + val toolbar = toolbarLayout.findViewById(R.id.toolbar) + toolbar.setTitleTextColor(toolbarTextColor) + toolbar.setNavigationIconTint(toolbarTextColor) + toolbar.overflowIcon?.setTint(toolbarTextColor) + setSupportActionBar(toolbar) title = getString(R.string.main_action_bar_title) + + // Set system status bar color and appearance + window.statusBarColor = statusBarColor + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = + Colors.shouldUseLightStatusBar(dynamicColors, darkMode) // Floating action button ("+") fab = findViewById(R.id.fab) fab.setOnClickListener { onSubscribeButtonClick() } + + // Add bottom padding to FAB to account for navigation bar + ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val layoutParams = view.layoutParams as androidx.constraintlayout.widget.ConstraintLayout.LayoutParams + layoutParams.bottomMargin = systemBars.bottom + view.layoutParams = layoutParams + insets + } // Swipe to refresh mainListContainer = findViewById(R.id.main_subscriptions_list_container) mainListContainer.setOnRefreshListener { refreshAllSubscriptions() } - mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator) + mainListContainer.setColorSchemeColors(Colors.swipeToRefreshColor(this)) // Update main list based on viewModel (& its datasource/livedata) val noEntries: View = findViewById(R.id.main_no_subscriptions) @@ -106,7 +183,15 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc val onSubscriptionLongClick = { s: Subscription -> onSubscriptionItemLongClick(s) } mainList = findViewById(R.id.main_subscriptions_list) - adapter = MainAdapter(repository, onSubscriptionClick, onSubscriptionLongClick) + adapter = MainAdapter( + repository, + onSubscriptionClick, + onSubscriptionLongClick, + ResourcesCompat.getDrawable(resources, R.drawable.ic_circle, theme)!!.apply { + setTint(Colors.primary(this@MainActivity)) + }, + Colors.onPrimary(this) + ) mainList.adapter = adapter viewModel.list().observe(this) { @@ -244,6 +329,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc } } + // 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 + // Create notification channels right away, so we can configure them immediately after installing the app dispatcher?.init() @@ -293,7 +382,19 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc val wsRemindTimeReached = repository.getWebSocketRemindTime() < System.currentTimeMillis() val showBanner = hasSelfHostedSubscriptions && wsRemindTimeReached && !usingWebSockets val wsBanner = findViewById(R.id.main_banner_websocket) - wsBanner.visibility = if (showBanner) View.VISIBLE else View.GONE + if (showBanner) { + wsBanner.visibility = View.VISIBLE + if (!BuildConfig.PAYMENT_LINKS_AVAILABLE) { + // Hide links that lead to payments, see https://github.com/binwiederhier/ntfy/issues/1463 + // This is a big fat hack, but I have to release this quickly ... + val wsBannerMainText = findViewById(R.id.main_banner_websocket_text) + val raw = getString(R.string.main_banner_websocket_text) + val unlinked = raw.replace(Regex("]*>"), "") + wsBannerMainText.text = HtmlCompat.fromHtml(unlinked, HtmlCompat.FROM_HTML_MODE_LEGACY) + } + } else { + wsBanner.visibility = View.GONE + } } private fun showHideWebSocketReconnectBanner(subscriptions: List) { @@ -372,6 +473,13 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_main_action_bar, menu) this.menu = menu + + // Tint menu icons based on theme + val toolbarTextColor = Colors.toolbarTextColor(this, repository.getDynamicColorsEnabled(), isDarkThemeOn(this)) + for (i in 0 until menu.size) { + menu[i].icon?.setTint(toolbarTextColor) + } + showHideNotificationMenuItems() checkSubscriptionsMuted() // This is done here, because then we know that we've initialized the menu return true @@ -412,9 +520,13 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc } val mutedUntilSeconds = repository.getGlobalMutedUntil() runOnUiThread { - // Show/hide in-app rate widget + // Show/hide menu items based on build config val rateAppItem = menu.findItem(R.id.main_menu_rate) + val docsItem = menu.findItem(R.id.main_menu_docs) + val reportBugItem = menu.findItem(R.id.main_menu_report_bug) rateAppItem.isVisible = BuildConfig.RATE_APP_AVAILABLE + docsItem.isVisible = BuildConfig.PAYMENT_LINKS_AVAILABLE // Google Payments Policy, see https://github.com/binwiederhier/ntfy/issues/1463 + reportBugItem.isVisible = BuildConfig.PAYMENT_LINKS_AVAILABLE // Google Payments Policy, see https://github.com/binwiederhier/ntfy/issues/1463 // Pause notification icons val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled) @@ -460,10 +572,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc } true } - R.id.main_menu_donate -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_donate_url)))) - true - } R.id.main_menu_docs -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_docs_url)))) true @@ -591,7 +699,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc } } } catch (e: Exception) { - val topic = displayName(subscription) + val topic = displayName(appBaseUrl, subscription) if (errorMessage == "") errorMessage = "$topic: ${e.message}" errors++ } @@ -618,7 +726,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc intent.putExtra(EXTRA_SUBSCRIPTION_ID, subscription.id) intent.putExtra(EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl) intent.putExtra(EXTRA_SUBSCRIPTION_TOPIC, subscription.topic) - intent.putExtra(EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(subscription)) + intent.putExtra(EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(appBaseUrl, subscription)) intent.putExtra(EXTRA_SUBSCRIPTION_INSTANT, subscription.instant) intent.putExtra(EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil) startActivity(intent) @@ -631,11 +739,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc intent.putExtra(DetailActivity.EXTRA_SUBSCRIPTION_ID, subscription.id) intent.putExtra(DetailActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl) intent.putExtra(DetailActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic) - intent.putExtra(DetailActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(subscription)) + intent.putExtra(DetailActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(appBaseUrl, subscription)) startActivity(intent) } - private fun handleActionModeClick(subscription: Subscription) { adapter.toggleSelection(subscription.id) if (adapter.selected.size == 0) { @@ -645,34 +752,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc } } - override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { - this.actionMode = mode - if (mode != null) { - mode.menuInflater.inflate(R.menu.menu_main_action_mode, menu) - mode.title = "1" // One item selected - } - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { - return false - } - - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { - return when (item?.itemId) { - R.id.main_action_mode_delete -> { - onMultiDeleteClick() - true - } - else -> false - } - } - private fun onMultiDeleteClick() { Log.d(DetailActivity.TAG, "Showing multi-delete dialog for selected items") - val builder = AlertDialog.Builder(this) - val dialog = builder + val dialog = MaterialAlertDialogBuilder(this) .setMessage(R.string.main_action_mode_delete_dialog_message) .setPositiveButton(R.string.main_action_mode_delete_dialog_permanently_delete) { _, _ -> adapter.selected.map { subscriptionId -> viewModel.remove(this, subscriptionId) } @@ -690,15 +773,11 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc dialog.show() } - override fun onDestroyActionMode(mode: ActionMode?) { - endActionModeAndRedraw() - } - private fun beginActionMode(subscription: Subscription) { - actionMode = startActionMode(this) + actionMode = startSupportActionMode(actionModeCallback) adapter.toggleSelection(subscription.id) - // Fade out FAB + // Fade out FAB fab.alpha = 1f fab .animate() @@ -709,11 +788,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc fab.visibility = View.GONE } }) - - // Fade status bar color - val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this)) - val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this)) - fadeStatusBarColor(window, fromColor, toColor) } private fun finishActionMode() { @@ -738,11 +812,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc fab.visibility = View.VISIBLE // Required to replace the old listener } }) - - // Fade status bar color - val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this)) - val toColor = ContextCompat.getColor(this, Colors.statusBarNormal(this)) - fadeStatusBarColor(window, fromColor, toColor) } private fun redrawList() { diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt index ed0d2bd0..d9d08434 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt @@ -2,6 +2,7 @@ package io.heckel.ntfy.ui import android.content.Context import android.graphics.Color +import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -20,7 +21,13 @@ import io.heckel.ntfy.util.readBitmapFromUriOrNull import java.text.DateFormat import java.util.* -class MainAdapter(private val repository: Repository, private val onClick: (Subscription) -> Unit, private val onLongClick: (Subscription) -> Unit) : +class MainAdapter( + private val repository: Repository, + private val onClick: (Subscription) -> Unit, + private val onLongClick: (Subscription) -> Unit, + private val countDrawable: Drawable, + private val onPrimaryColor: Int +) : ListAdapter(TopicDiffCallback) { val selected = mutableSetOf() // Subscription IDs @@ -28,7 +35,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.fragment_main_item, parent, false) - return SubscriptionViewHolder(view, repository, selected, onClick, onLongClick) + return SubscriptionViewHolder(view, repository, selected, onClick, onLongClick, countDrawable, onPrimaryColor) } /* Gets current topic and uses it to bind view. */ @@ -52,7 +59,15 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs } /* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */ - class SubscriptionViewHolder(itemView: View, private val repository: Repository, private val selected: Set, val onClick: (Subscription) -> Unit, val onLongClick: (Subscription) -> Unit) : + class SubscriptionViewHolder( + itemView: View, + private val repository: Repository, + private val selected: Set, + val onClick: (Subscription) -> Unit, + val onLongClick: (Subscription) -> Unit, + private val countDrawable: Drawable, + private val onPrimaryColor: Int + ) : RecyclerView.ViewHolder(itemView) { private var subscription: Subscription? = null private val context: Context = itemView.context @@ -64,6 +79,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs private val notificationDisabledForeverImageView: View = itemView.findViewById(R.id.main_item_notification_disabled_forever_image) private val instantImageView: View = itemView.findViewById(R.id.main_item_instant_image) private val newItemsView: TextView = itemView.findViewById(R.id.main_item_new) + private val appBaseUrl = context.getString(R.string.app_base_url) fun bind(subscription: Subscription) { this.subscription = subscription @@ -99,7 +115,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs } else { imageView.setImageResource(R.drawable.ic_sms_gray_24dp) } - nameView.text = displayName(subscription) + nameView.text = displayName(appBaseUrl, subscription) statusView.text = statusMessage dateView.text = dateText dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE @@ -111,11 +127,13 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs } else { newItemsView.visibility = View.VISIBLE newItemsView.text = if (subscription.newCount <= 99) subscription.newCount.toString() else "99+" + newItemsView.setTextColor(onPrimaryColor) + newItemsView.background = countDrawable } itemView.setOnClickListener { onClick(subscription) } itemView.setOnLongClickListener { onLongClick(subscription); true } if (selected.contains(subscription.id)) { - itemView.setBackgroundResource(Colors.itemSelectedBackground(context)) + itemView.setBackgroundColor(Colors.itemSelectedBackground(context)) } else { itemView.setBackgroundColor(Color.TRANSPARENT) } diff --git a/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt index 2d84879c..920d1bd4 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt @@ -7,6 +7,7 @@ import android.os.Bundle import android.widget.RadioButton import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.heckel.ntfy.R import io.heckel.ntfy.db.Repository import kotlinx.coroutines.Dispatchers @@ -74,7 +75,7 @@ class NotificationFragment : DialogFragment() { muteForeverButton = view.findViewById(R.id.notification_dialog_forever) muteForeverButton.setOnClickListener{ onClick(Repository.MUTED_UNTIL_FOREVER) } - return AlertDialog.Builder(activity) + return MaterialAlertDialogBuilder(requireContext()) .setView(view) .create() } 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 2c73ef27..170fede3 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -2,7 +2,6 @@ package io.heckel.ntfy.ui import android.Manifest import android.app.AlarmManager -import android.app.AlertDialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -13,6 +12,7 @@ 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.result.contract.ActivityResultContracts @@ -21,10 +21,12 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import androidx.preference.* import androidx.preference.Preference.OnPreferenceClickListener +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.gson.Gson import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R @@ -65,6 +67,28 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere repository = Repository.getInstance(this) serviceManager = SubscriberServiceManager(this) + val toolbarLayout = findViewById(R.id.app_bar_drawer) + val dynamicColors = repository.getDynamicColorsEnabled() + val darkMode = isDarkThemeOn(this) + val statusBarColor = Colors.statusBarNormal( + this, + dynamicColors, + darkMode + ) + val toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode) + toolbarLayout.setBackgroundColor(statusBarColor) + + val toolbar = toolbarLayout.findViewById(R.id.toolbar) + toolbar.setTitleTextColor(toolbarTextColor) + toolbar.setNavigationIconTint(toolbarTextColor) + toolbar.overflowIcon?.setTint(toolbarTextColor) + setSupportActionBar(toolbar) + + // Set system status bar color and appearance + window.statusBarColor = statusBarColor + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = + Colors.shouldUseLightStatusBar(dynamicColors, darkMode) + if (savedInstanceState == null) { settingsFragment = SettingsFragment() // Empty constructor! supportFragmentManager @@ -128,7 +152,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } } - class SettingsFragment : PreferenceFragmentCompat() { + class SettingsFragment : BasePreferenceFragment() { private lateinit var repository: Repository private lateinit var serviceManager: SubscriberServiceManager private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET @@ -211,7 +235,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere // Keep alerting for max priority val insistentMaxPriorityPrefId = context?.getString(R.string.settings_notifications_insistent_max_priority_key) ?: return - val insistentMaxPriority: SwitchPreference? = findPreference(insistentMaxPriorityPrefId) + val insistentMaxPriority: SwitchPreferenceCompat? = findPreference(insistentMaxPriorityPrefId) insistentMaxPriority?.isChecked = repository.getInsistentMaxPriorityEnabled() insistentMaxPriority?.preferenceDataStore = object : PreferenceDataStore() { override fun putBoolean(key: String?, value: Boolean) { @@ -221,7 +245,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere return repository.getInsistentMaxPriorityEnabled() } } - insistentMaxPriority?.summaryProvider = Preference.SummaryProvider { pref -> + insistentMaxPriority?.summaryProvider = Preference.SummaryProvider { pref -> if (pref.isChecked) { getString(R.string.settings_notifications_insistent_max_priority_summary_enabled) } else { @@ -324,11 +348,45 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } } + // Dynamic colors + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val dynamicColorsEnabledPrefId = context?.getString(R.string.settings_general_dynamic_colors_key) ?: return + val dynamicColorsEnabled: SwitchPreferenceCompat? = findPreference(dynamicColorsEnabledPrefId) + dynamicColorsEnabled?.isChecked = repository.getDynamicColorsEnabled() + dynamicColorsEnabled?.preferenceDataStore = object : PreferenceDataStore() { + override fun putBoolean(key: String?, value: Boolean) { + repository.setDynamicColorsEnabled(value) + + // Restart app + val packageManager = requireContext().packageManager + val packageName = requireContext().packageName + val intent = packageManager.getLaunchIntentForPackage(packageName) + val componentName = intent!!.component + val mainIntent = Intent.makeRestartActivityTask(componentName) + startActivity(mainIntent) + Runtime.getRuntime().exit(0) + } + override fun getBoolean(key: String?, defValue: Boolean): Boolean { + return repository.getDynamicColorsEnabled() + } + } + dynamicColorsEnabled?.summaryProvider = Preference.SummaryProvider { pref -> + if (pref.isChecked) { + getString(R.string.settings_general_dynamic_colors_summary_enabled) + } else { + getString(R.string.settings_general_dynamic_colors_summary_disabled) + } + } + dynamicColorsEnabled?.isVisible = true + } + // Default Base URL val appBaseUrl = getString(R.string.app_base_url) val defaultBaseUrlPrefId = context?.getString(R.string.settings_general_default_base_url_key) ?: return val defaultBaseUrl: EditTextPreference? = findPreference(defaultBaseUrlPrefId) defaultBaseUrl?.text = repository.getDefaultBaseUrl() ?: "" + defaultBaseUrl?.extras?.putString("message", getString(R.string.settings_general_default_base_url_message)) + defaultBaseUrl?.extras?.putString("hint", getString(R.string.app_base_url)) defaultBaseUrl?.preferenceDataStore = object : PreferenceDataStore() { override fun putString(key: String, value: String?) { val baseUrl = value ?: return @@ -355,7 +413,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere // Broadcast enabled val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return - val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId) + val broadcastEnabled: SwitchPreferenceCompat? = findPreference(broadcastEnabledPrefId) broadcastEnabled?.isChecked = repository.getBroadcastEnabled() broadcastEnabled?.preferenceDataStore = object : PreferenceDataStore() { override fun putBoolean(key: String?, value: Boolean) { @@ -365,7 +423,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere return repository.getBroadcastEnabled() } } - broadcastEnabled?.summaryProvider = Preference.SummaryProvider { pref -> + broadcastEnabled?.summaryProvider = Preference.SummaryProvider { pref -> if (pref.isChecked) { getString(R.string.settings_advanced_broadcast_summary_enabled) } else { @@ -375,7 +433,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere // Enable UnifiedPush val unifiedPushEnabledPrefId = context?.getString(R.string.settings_advanced_unifiedpush_key) ?: return - val unifiedPushEnabled: SwitchPreference? = findPreference(unifiedPushEnabledPrefId) + val unifiedPushEnabled: SwitchPreferenceCompat? = findPreference(unifiedPushEnabledPrefId) unifiedPushEnabled?.isChecked = repository.getUnifiedPushEnabled() unifiedPushEnabled?.preferenceDataStore = object : PreferenceDataStore() { override fun putBoolean(key: String?, value: Boolean) { @@ -385,7 +443,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere return repository.getUnifiedPushEnabled() } } - unifiedPushEnabled?.summaryProvider = Preference.SummaryProvider { pref -> + unifiedPushEnabled?.summaryProvider = Preference.SummaryProvider { pref -> if (pref.isChecked) { getString(R.string.settings_advanced_unifiedpush_summary_enabled) } else { @@ -420,7 +478,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere // Record logs val recordLogsPrefId = context?.getString(R.string.settings_advanced_record_logs_key) ?: return - val recordLogsEnabled: SwitchPreference? = findPreference(recordLogsPrefId) + val recordLogsEnabled: SwitchPreferenceCompat? = findPreference(recordLogsPrefId) recordLogsEnabled?.isChecked = Log.getRecord() recordLogsEnabled?.preferenceDataStore = object : PreferenceDataStore() { override fun putBoolean(key: String?, value: Boolean) { @@ -433,7 +491,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere return Log.getRecord() } } - recordLogsEnabled?.summaryProvider = Preference.SummaryProvider { pref -> + recordLogsEnabled?.summaryProvider = Preference.SummaryProvider { pref -> if (pref.isChecked) { getString(R.string.settings_advanced_record_logs_summary_enabled) } else { @@ -670,7 +728,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } else { getString(R.string.settings_advanced_export_logs_scrub_dialog_empty) } - val dialog = AlertDialog.Builder(activity) + val dialog = MaterialAlertDialogBuilder(requireContext()) .setTitle(title) .setMessage(scrubbedText) .setPositiveButton(R.string.settings_advanced_export_logs_scrub_dialog_button_ok) { _, _ -> /* Nothing */ } @@ -711,7 +769,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere data class NopasteResponse(val url: String) } - class UserSettingsFragment : PreferenceFragmentCompat() { + class UserSettingsFragment : BasePreferenceFragment() { private lateinit var repository: Repository override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { diff --git a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt index dc219ed1..c3d11ea7 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt @@ -9,6 +9,7 @@ import android.text.TextWatcher import android.view.* import android.widget.* import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.textfield.TextInputLayout @@ -18,6 +19,8 @@ import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import androidx.core.view.size +import androidx.core.view.get class ShareActivity : AppCompatActivity() { private val repository by lazy { (application as Application).repository } @@ -55,7 +58,24 @@ class ShareActivity : AppCompatActivity() { Log.d(TAG, "Create $this with intent $intent") // Action bar + val toolbarLayout = findViewById(R.id.app_bar_drawer) + val dynamicColors = repository.getDynamicColorsEnabled() + val darkMode = isDarkThemeOn(this) + val statusBarColor = Colors.statusBarNormal(this, dynamicColors, darkMode) + val toolbarTextColor = Colors.toolbarTextColor(this, dynamicColors, darkMode) + toolbarLayout.setBackgroundColor(statusBarColor) + + val toolbar = toolbarLayout.findViewById(R.id.toolbar) + toolbar.setTitleTextColor(toolbarTextColor) + toolbar.setNavigationIconTint(toolbarTextColor) + toolbar.overflowIcon?.setTint(toolbarTextColor) + setSupportActionBar(toolbar) title = getString(R.string.share_title) + + // Set system status bar color and appearance + window.statusBarColor = statusBarColor + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = + Colors.shouldUseLightStatusBar(dynamicColors, darkMode) // Show 'Back' button supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -235,6 +255,13 @@ class ShareActivity : AppCompatActivity() { menuInflater.inflate(R.menu.menu_share_action_bar, menu) this.menu = menu sendItem = menu.findItem(R.id.share_menu_send) + + // Tint menu icons based on theme + val toolbarTextColor = Colors.toolbarTextColor(this, repository.getDynamicColorsEnabled(), isDarkThemeOn(this)) + for (i in 0 until menu.size) { + menu[i].icon?.setTint(toolbarTextColor) + } + validateInput() // Disable icon return true } diff --git a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt index 6da8304a..e32b88fc 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt @@ -9,7 +9,9 @@ import android.view.WindowManager import android.widget.Button import android.widget.TextView import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout import io.heckel.ntfy.R import io.heckel.ntfy.db.User import io.heckel.ntfy.util.AfterChangedTextWatcher @@ -21,6 +23,7 @@ class UserFragment : DialogFragment() { private lateinit var baseUrlsInUse: ArrayList private lateinit var listener: UserDialogListener + private lateinit var baseUrlViewLayout: TextInputLayout private lateinit var baseUrlView: TextInputEditText private lateinit var usernameView: TextInputEditText private lateinit var passwordView: TextInputEditText @@ -54,9 +57,10 @@ class UserFragment : DialogFragment() { val view = requireActivity().layoutInflater.inflate(R.layout.fragment_user_dialog, null) val positiveButtonTextResId = if (user == null) R.string.user_dialog_button_add else R.string.user_dialog_button_save - val titleView = view.findViewById(R.id.user_dialog_title) as TextView - val descriptionView = view.findViewById(R.id.user_dialog_description) as TextView + val titleView = view.findViewById(R.id.user_dialog_title) + val descriptionView = view.findViewById(R.id.user_dialog_description) + baseUrlViewLayout = view.findViewById(R.id.user_dialog_base_url_layout) baseUrlView = view.findViewById(R.id.user_dialog_base_url) usernameView = view.findViewById(R.id.user_dialog_username) passwordView = view.findViewById(R.id.user_dialog_password) @@ -64,18 +68,18 @@ class UserFragment : DialogFragment() { if (user == null) { titleView.text = getString(R.string.user_dialog_title_add) descriptionView.text = getString(R.string.user_dialog_description_add) - baseUrlView.visibility = View.VISIBLE + baseUrlViewLayout.visibility = View.VISIBLE passwordView.hint = getString(R.string.user_dialog_password_hint_add) } else { titleView.text = getString(R.string.user_dialog_title_edit) descriptionView.text = getString(R.string.user_dialog_description_edit) - baseUrlView.visibility = View.GONE + baseUrlViewLayout.visibility = View.GONE usernameView.setText(user!!.username) passwordView.hint = getString(R.string.user_dialog_password_hint_edit) } // Build dialog - val builder = AlertDialog.Builder(activity) + val builder = MaterialAlertDialogBuilder(requireContext()) .setView(view) .setPositiveButton(positiveButtonTextResId) { _, _ -> saveClicked() diff --git a/app/src/main/java/io/heckel/ntfy/up/Constants.kt b/app/src/main/java/io/heckel/ntfy/up/Constants.kt index ab9c8160..7db2680e 100644 --- a/app/src/main/java/io/heckel/ntfy/up/Constants.kt +++ b/app/src/main/java/io/heckel/ntfy/up/Constants.kt @@ -13,9 +13,8 @@ const val ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE" const val ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER" const val ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER" -const val FEATURE_BYTES_MESSAGE = "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE" - const val EXTRA_APPLICATION = "application" +const val EXTRA_PI = "pi" const val EXTRA_TOKEN = "token" const val EXTRA_ENDPOINT = "endpoint" const val EXTRA_MESSAGE = "message" diff --git a/app/src/main/java/io/heckel/ntfy/up/LinkActivity.kt b/app/src/main/java/io/heckel/ntfy/up/LinkActivity.kt new file mode 100644 index 00000000..bbffae60 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/up/LinkActivity.kt @@ -0,0 +1,36 @@ +package io.heckel.ntfy.up + +import android.app.Activity +import android.app.PendingIntent +import android.content.Intent +import android.os.Bundle +import android.util.Log + +/** + * This implements the "Select default distributor" selection for UnifiedPush. + * + * To test, install ntfy and another distributor (e.g. SunUp) on the same phone. + * Install an app that uses UnifiedPush (e.g. UP Example) and click "Register". + * + * You should see a popup to select the default distributor. + * See https://unifiedpush.org/developers/spec/android/#link-activity + */ +class LinkActivity: Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + intent?.data?.run { + Log.d(TAG, "Received request for $callingPackage") + val intent = Intent("org.unifiedpush.register.dummy_app") + val pendingIntent = PendingIntent.getBroadcast(this@LinkActivity, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val result = Intent().apply { + putExtra(EXTRA_PI, pendingIntent) + } + setResult(RESULT_OK, result) + } ?: setResult(RESULT_CANCELED) + finish() + } + + companion object { + private val TAG = LinkActivity::class.simpleName + } +} diff --git a/app/src/main/java/io/heckel/ntfy/util/MarkwonFactory.kt b/app/src/main/java/io/heckel/ntfy/util/MarkwonFactory.kt index c9630683..65b6aaef 100644 --- a/app/src/main/java/io/heckel/ntfy/util/MarkwonFactory.kt +++ b/app/src/main/java/io/heckel/ntfy/util/MarkwonFactory.kt @@ -1,12 +1,10 @@ package io.heckel.ntfy.util import android.content.Context -import android.graphics.Paint import android.graphics.Typeface import android.text.style.* import android.text.util.Linkify -import androidx.core.content.ContextCompat -import io.heckel.ntfy.R +import io.heckel.ntfy.ui.Colors import io.noties.markwon.* import io.noties.markwon.core.CorePlugin import io.noties.markwon.core.CoreProps @@ -36,7 +34,7 @@ internal object MarkwonFactory { .usePlugin(object : AbstractMarkwonPlugin() { override fun configureTheme(builder: MarkwonTheme.Builder) { builder - .linkColor(ContextCompat.getColor(context, R.color.teal)) + .linkColor(Colors.linkColor(context)) .isLinkUnderlined(true) } 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 cf6f34cb..017830a8 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -1,7 +1,5 @@ package io.heckel.ntfy.util -import android.animation.ArgbEvaluator -import android.animation.ValueAnimator import android.content.ClipData import android.content.ClipboardManager import android.content.ContentResolver @@ -20,11 +18,9 @@ import android.text.Editable import android.text.TextWatcher import android.util.Base64 import android.view.View -import android.view.Window import android.widget.Button import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.ContextCompat import io.heckel.ntfy.R import io.heckel.ntfy.db.ACTION_PROGRESS_FAILED import io.heckel.ntfy.db.ACTION_PROGRESS_ONGOING @@ -68,8 +64,13 @@ fun subscriptionTopicShortUrl(subscription: Subscription) : String { return topicShortUrl(subscription.baseUrl, subscription.topic) } -fun displayName(subscription: Subscription) : String { - return subscription.displayName ?: subscriptionTopicShortUrl(subscription) +fun displayName(appBaseUrl: String?, subscription: Subscription) : String { + if (subscription.displayName != null) { + return subscription.displayName + } else if (appBaseUrl == subscription.baseUrl) { + return subscription.topic + } + return subscriptionTopicShortUrl(subscription) } fun shortUrl(url: String) = url @@ -190,11 +191,11 @@ fun decodeBytesMessage(notification: Notification): ByteArray { * See above; prepend emojis to title if the title is non-empty. * Otherwise, they are prepended to the message. */ -fun formatTitle(subscription: Subscription, notification: Notification): String { +fun formatTitle(appBaseUrl: String?, subscription: Subscription, notification: Notification): String { return if (notification.title != "") { formatTitle(notification) } else { - displayName(subscription) + displayName(appBaseUrl, subscription) } } @@ -276,16 +277,6 @@ data class FileInfo( val size: Long, ) -// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785 -fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) { - val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor) - statusBarColorAnimation.addUpdateListener { animator -> - val color = animator.animatedValue as Int - window.statusBarColor = color - } - statusBarColorAnimation.start() -} - // Generates a (cryptographically secure) random string of a certain length fun randomString(len: Int): String { val random = SecureRandom() @@ -344,10 +335,7 @@ fun supportedImage(mimeType: String?): Boolean { // Play didn't grant us the permission, and F-Droid users didn't want us to have it. // See https://github.com/binwiederhier/ntfy/issues/531 & https://github.com/binwiederhier/ntfy/issues/684 fun canOpenAttachment(attachment: Attachment?): Boolean { - if (attachment?.type == ANDROID_APP_MIME_TYPE) { - return false - } - return true + return attachment?.type != ANDROID_APP_MIME_TYPE } // Check if battery optimization is enabled, see https://stackoverflow.com/a/49098293/1440785 @@ -507,11 +495,10 @@ 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))) + setTextColor(Colors.dangerText(context)) } } fun Long.nullIfZero(): Long? { return if (this == 0L) return null else this } - diff --git a/app/src/main/res/anim/slide_in_bottom.xml b/app/src/main/res/anim/slide_in_bottom.xml new file mode 100644 index 00000000..dc163787 --- /dev/null +++ b/app/src/main/res/anim/slide_in_bottom.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/anim/slide_out_bottom.xml b/app/src/main/res/anim/slide_out_bottom.xml new file mode 100644 index 00000000..362a7564 --- /dev/null +++ b/app/src/main/res/anim/slide_out_bottom.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_bolt_outline_white_24dp.xml b/app/src/main/res/drawable/ic_bolt_outline_white_24dp.xml index 9ecde1ff..1a91ebc8 100644 --- a/app/src/main/res/drawable/ic_bolt_outline_white_24dp.xml +++ b/app/src/main/res/drawable/ic_bolt_outline_white_24dp.xml @@ -1,6 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/ic_notifications_off_time_white_outline_24dp.xml b/app/src/main/res/drawable/ic_notifications_off_time_white_outline_24dp.xml index f3c0ad0b..09425d1c 100644 --- a/app/src/main/res/drawable/ic_notifications_off_time_white_outline_24dp.xml +++ b/app/src/main/res/drawable/ic_notifications_off_time_white_outline_24dp.xml @@ -1,6 +1,7 @@ - + + + + - + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d532db64..1bf20371 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,290 +1,309 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" /> - + - - + + + + + + + + + + + + + + + + + + + + - + - + - + - - - + + + - - - - - - - - - - - - + style="@style/BannerCardStyle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/main_banner_websocket" + android:id="@+id/main_banner_websocket_reconnect" android:visibility="visible"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index f02c645d..087faaa2 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,9 +1,27 @@ - + + - + + + + - + android:layout_height="match_parent" /> + + + + diff --git a/app/src/main/res/layout/activity_share.xml b/app/src/main/res/layout/activity_share.xml index dae99ae5..fd5ea44c 100644 --- a/app/src/main/res/layout/activity_share.xml +++ b/app/src/main/res/layout/activity_share.xml @@ -1,9 +1,23 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + + + + @@ -163,4 +179,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/share_error_text" android:layout_marginTop="2dp"/> - + + + + diff --git a/app/src/main/res/layout/app_bar_drawer.xml b/app/src/main/res/layout/app_bar_drawer.xml new file mode 100644 index 00000000..f61c73b6 --- /dev/null +++ b/app/src/main/res/layout/app_bar_drawer.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_add_dialog.xml b/app/src/main/res/layout/fragment_add_dialog.xml index ad0c7d49..910d1678 100644 --- a/app/src/main/res/layout/fragment_add_dialog.xml +++ b/app/src/main/res/layout/fragment_add_dialog.xml @@ -1,73 +1,121 @@ - - - - + - - + + + + + + + + + + - - - + + + + + + + + + + + + + + - + + - - + android:textAppearance="?android:attr/textAppearanceMedium"/> - - - - - + + + + + + + + + + - - - + + - - - + + + + - - + + - - - - - - - - - + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_user_dialog.xml b/app/src/main/res/layout/fragment_user_dialog.xml index f6ed2715..a2e982fe 100644 --- a/app/src/main/res/layout/fragment_user_dialog.xml +++ b/app/src/main/res/layout/fragment_user_dialog.xml @@ -1,55 +1,93 @@ - - + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingHorizontal="?dialogPreferredPadding" + android:visibility="visible"> - + + + + + + - + + + + + + - + + + + + + + android:maxLines="1" + android:inputType="textPassword"/> + + diff --git a/app/src/main/res/layout/preference_dialog_edittext_edited.xml b/app/src/main/res/layout/preference_dialog_edittext_edited.xml index b55bb63e..f4b36fc8 100644 --- a/app/src/main/res/layout/preference_dialog_edittext_edited.xml +++ b/app/src/main/res/layout/preference_dialog_edittext_edited.xml @@ -45,15 +45,18 @@ android:layout_height="wrap_content" android:textColor="?android:attr/textColorSecondary"/> - + android:layout_marginHorizontal="?dialogPreferredPadding" + android:paddingTop="?dialogPreferredPadding"> + + + + diff --git a/app/src/main/res/layout/view_preference_switch.xml b/app/src/main/res/layout/view_preference_switch.xml new file mode 100644 index 00000000..ba3fd967 --- /dev/null +++ b/app/src/main/res/layout/view_preference_switch.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_add_dialog.xml b/app/src/main/res/menu/menu_add_dialog.xml new file mode 100644 index 00000000..13ea08fd --- /dev/null +++ b/app/src/main/res/menu/menu_add_dialog.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/menu/menu_detail_action_bar.xml b/app/src/main/res/menu/menu_detail_action_bar.xml index 0b76cf93..b90c71fb 100644 --- a/app/src/main/res/menu/menu_detail_action_bar.xml +++ b/app/src/main/res/menu/menu_detail_action_bar.xml @@ -1,17 +1,44 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_detail_action_mode.xml b/app/src/main/res/menu/menu_detail_action_mode.xml index 9da6be11..8d45bc3e 100644 --- a/app/src/main/res/menu/menu_detail_action_mode.xml +++ b/app/src/main/res/menu/menu_detail_action_mode.xml @@ -1,6 +1,14 @@ - - - + + + diff --git a/app/src/main/res/menu/menu_main_action_bar.xml b/app/src/main/res/menu/menu_main_action_bar.xml index 75068746..7d7ffec1 100644 --- a/app/src/main/res/menu/menu_main_action_bar.xml +++ b/app/src/main/res/menu/menu_main_action_bar.xml @@ -1,13 +1,31 @@ - - - - - - - - - + + + + + + + + diff --git a/app/src/main/res/menu/menu_main_action_mode.xml b/app/src/main/res/menu/menu_main_action_mode.xml index 681733e0..c58056ac 100644 --- a/app/src/main/res/menu/menu_main_action_mode.xml +++ b/app/src/main/res/menu/menu_main_action_mode.xml @@ -1,4 +1,9 @@ - - + + diff --git a/app/src/main/res/menu/menu_share_action_bar.xml b/app/src/main/res/menu/menu_share_action_bar.xml index 6321e1ad..530b0a11 100644 --- a/app/src/main/res/menu/menu_share_action_bar.xml +++ b/app/src/main/res/menu/menu_share_action_bar.xml @@ -1,4 +1,9 @@ - - + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2a55c732..9cac9d37 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -66,7 +66,6 @@ كل شئ محدث لاخر تحديث تم الاشتراك في 1 موضوع فوري تم الاشتراك في 4 مواضيع فورية - تبرع 💸 اﻹعدادات تعذر تحديث %1$d اشتراكات \n diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 36423ca6..c0ef6c8a 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -327,7 +327,6 @@ Относно Адрес на темата Копирано в междинната памет - Даряване 💸 Ntfy не може да инсталира получени приложения. Вместо това изтеглете чрез браузъра. За подробности вижте дефект №531. Подразбирани Потребителски настройки за известия diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 9ce0a931..edfd4376 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -6,7 +6,6 @@ Prioritat alta Prioritat màxima Servei Subscripció - Donar 💸 reconnectant… Escoltant notificacions entrants Subscrit per entrega instantània de temes diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0dc1d044..665dac73 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -327,7 +327,6 @@ Povolit nyní 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 další konfiguraci v proxy serveru. Metodu připojení lze přepnout v Nastavení. Zvolit URL služby - Přispět 💸 Aplikace již nelze nainstalovat. Místo toho stahujte přes prohlížeč. Podrobnosti naleznete v issue #531. Výchozí Upozornění s nejvyšší prioritou pouze jednou diff --git a/app/src/main/res/values-cu/strings.xml b/app/src/main/res/values-cu/strings.xml new file mode 100644 index 00000000..61edacf9 --- /dev/null +++ b/app/src/main/res/values-cu/strings.xml @@ -0,0 +1,9 @@ + + + Низкое преимѹ́щество + Мин преимѹ́щество + Слꙋ́жба подписки + Ѻбыденное преимѹ́щество + Высо́кое преимѹ́щество + Макс преимѹ́щество + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 1a8bbb75..aee3c4d1 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -17,4 +17,81 @@ Abonnerer på et emne med øjeblikkelig levering Abonnerer på fire emner med øjeblikkelig levering Høj prioritet - \ No newline at end of file + Abonnerer på et emne + Abonnerer på to emner + Abonnerer på tre emner + Abonnerer på fire emner + Abonnerer på fem emner + Abonnerer på seks emner + Abonnerer på %1$d emner + %1$d notifikation(er) modtaget + Alt er opdateret + Kunne ikke opdaterer %1$d abonnementer\n\n%2$s + Kunne ikke genopfriske abonnement %1$s + Notifikationer aktiveret + Notifikationer slået fra + Notifikationer slået fra indtil %1$s + Indstillinger + Anmeld fejl + Læs manualen + Anmeld appen ⭐ + Afmeld + Afmeld valgte emne(r) og slet alle notifikationer permanent? + Slet permanent + Annuller + %1$d notifikationer + %1$d notifikationer + Tilslutter … + %1$s (UnifiedPush) + i går + Tilføj abonomment + Det ser ud til at du ikke abonnere på noget endnu. + Klik + for at oprette eller abonnere på et emne. Derefter kan du modtage notifikationer på din enhed vha. PUT og POST. + Detaljeret instruktioner tilgængelige på ntfy.sh, og i manualen. + Dette abonnement er styret af %1$s vha. UnifiedPush + Batterioptimering bør være slået fra for appen for at undgå problemer med at modtage notifikationer. + Spørg senere + Afvis + Løs nu + Anvendelse af WebSockets er den anbefalede måde tilslutte dig din server, og kan forbedre batterilevetiden, men det kan kræve yderligere konfigurationer i din proxy. Dette kan ændres i indstillingerne. + Spørg senere + Afvis + Aktiver nu + For at kunne garantere at WebSockets genopretter forbindelsen i baggrunden, skal du give Alarm & Påmindelses tilladelser til ntfy + Spørg senere + Afvis + Tildel nu + Abonner på emne + Emner er ikke password-beskyttet, så vælg et navn der er svært at gætte. Når først du er abonnere, kan du PUT/POST notifikationer. + Emne navn, f.eks. jørns_alarmer + Brug anden server + Skriv URLs herunder for at abonnere på emner fra andre servere. + Øjeblikkelig levering i dvale + Garanter at meldingerne bliver leveret med det samme, selv hvis enheden er inaktiv. + Øjeblikkelig levering er altid aktiveret for andre værter end %1$s. + Annuller + Abonner + Tilbage + Log ind + Tilslutning fejlede: %1$s + Login krævet + Dette emne kræver at du logger ind. Skriv venligst dit brugernavn og password. + Brugernavn + Password + Login fejlede. Bruger %1$s er ikke autoriseret. + Ny bruger + Vælg service URL + Fjern service URL + Du har ikke modtaget nogen notifikationer for dette emne endnu. + For at sende notifikationer for dette emne, PUT eller POST til emne URLen. + $ curl -d \"Hej\"%1$s]]> + Detaljeret instruktioner tilgængelig på ntfy.sh, and in the docs. + Slet alle notifikationer for emnet? + Slet permanent + Annuller + Afmeld abonnementet for dette emne og slet alle modtagende notifikationer? + Slet permanent + Annuller + Test: Ændre titlen til det du vil. + Dette er en test notifikation fra ntfy Android app. Den har prioritets niveau %1$d. Hvis du sender en anden, kan den se anderledes ud. + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 177194c1..9cf1b474 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -327,7 +327,6 @@ Themen-URL Über In Zwischenablage kopiert - Spenden 💸 Apps können nicht mehr installiert werden. Bitte stattdessen über einen Browser herunterladen. Details siehe Issue #531. Eigene Einstellungen für dieses Abo verwenden Beanchrichtigungseinstellungen konfigurieren diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 6eb1ce1d..0096d259 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -60,7 +60,6 @@ Όνομα θέματος Χρήση άλλου server/εξυπηρετητή Εγγραφή σε ειδοποίηση - Κάντε δωρεά Η μετάβαση σε WebSockets είναι ο συνιστώμενος τρόπος σύνδεσης με τον διακομιστή σας και μπορεί να βελτιώσει τη διάρκεια ζωής της μπαταρίας, αλλά μπορεί να απαιτεί πρόσθετες ρυθμίσεις στο διακομιστή μεσολάβησης. Αυτό μπορεί να ενεργοποιηθεί στις Ρυθμίσεις. Κωδικός πρόσβασης Η σύνδεση απέτυχε. Ο χρήστης %1$s δεν είναι εξουσιοδοτημένος. diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index a6b3daec..919f68c7 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Min prioritato + Malalta prioritato + Defaŭlta prioritato + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9a0c5a31..076fcdad 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -327,7 +327,6 @@ Borrar la URL del servicio 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 configuración adicional en su proxy. Esto se puede cambiar en la Configuración. Habilitar ahora - Donar 💸 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. Predeterminado Mantener alertas para la máxima prioridad diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index d2dcab01..b452d70d 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -6,7 +6,6 @@ Teata vigadest Loe dokumentatsiooni Hinda rakendust ⭐ - Toeta arendajat 💸 Kustuta jäädavalt %1$d teavitust %1$s (UnifiedPush) @@ -274,4 +273,70 @@ Taastamine õnnestus Taastamine ei õnnestunud: %1$s Täiendavad seadistused + Leviedasta sõnumeid + Rakendused saavad teavitusi leviedastusena (kõik saavad sama sõnumi) + Rakendused ei saa teavitusi leviedastusena + Kasuta UnifiedPushi + ntfy toimib UnifiedPushi levitajana + ntfy ei toimi UnifiedPushi levitajana + Salvesta logisid + Login (kuni 1,000 kirjet) seadmesse… + Lülita logimine sisse ja sa saad neid vigade otsimisel jagada. + Kopeeri logid või laadi nad üles + Kopeeri logid lõikelaule või laadi nad nopaste.net teenusesse (mille omanik on ntfy autor). Seadmete nimed ja aadressid on võimalik välja jätta, kui teavituste sisusid mitte. + Kopeeri lõikelauale + Kopeeri lõikelauale (tsenseerituna) + Laadi üles ja kopeeri link + Laadi üles ja kopeeri link (tsenseerituna) + Logid on kopeeritud lõikelauale + Logid on üleslaadimisel… + Logid on laaditud üles ja võrguaadress on kopeeritud + Logide üleslaadimine ei õnnestunud: %1$s + Need teemad ja seadmete nimed on asendatud puuviljade nimedega ja seega saad ohutumalt logi jagada:\n\n%1$s\n\nKa salasõnad on korjatud välja, kuid neid pole siin näidatud. + Ühtegi teemat ega seadme nime polnud asendatud. Kas sul üldse on teemade tellimusi? + Sobib + Kustuta logid + Kustuta varasemad logid ja alusta nullist + Logid on kustutatud + Ühendusprotokoll + Ühenduseks serveriga kasuta JSON-i voogedastust üle HTTP. See meetod on korralikult testitud, aga võib suurendada akukasutust. + Ühenduseks serveriga kasuta WebSocketsi protokolli. Selle meetodi kasutamine on esimene soovitus, aga see võib eeldada sinu proksiserveri täiendavat seadistamist. + JSON-i voogedastus üle HTTP + WebSockets + Kasuta üldist seadistust + kasutan üldist seadistust + Rakenduse teave + Lisa kasutaja + Muuda kasutajat + Sa võid muuta valitud kasutaja kasutajanime või salasõna, aga ta ka sootuks kustutada. + ntfy %1$s (%2$s) + Kopeeritud lõikelauale + Teenuse võrguaadress + Kasutajanimi + Salasõna + Salasõna (kui jääb tühjaks, siis ei muutu) + Lisa kasutaja + Katkesta + Kustuta kasutaja + Salvesta + Täpsed äratused + ntfy võib ajastada täpseid äratusi. Need on vajalikud WebSocketsi toimimiseks taustal. Klõpsa selle õiguse keelamiseks. + ntfy ei või ajastada täpseid äratusi. Need on vajalikud WebSocketsi toimimiseks taustal. Klõpsa selle õiguse lubamiseks. + Rakenduse teave + Versioon + Ikooni salvestamine ei õnnestu: %1$s + Kuvatav nimi + Määra selle tellimuse jaoks eraldi kuvatav nimi. Vaikimisi nime jaoks jäta tühjaks (%1$s). + %1$s (vaikimisi) + Välimus + Tellimuse ikoon + Vali teavitustes kuvatav ikoon + Tellimuste ikoon (eemaldamiseks klõpsa) + Teavituste kohanadatud seadistused + Kasutan selle tellimuse jaoks kohandatud teavitusi + Kasutan vaikimisi teavitusi (helimärguanded, „Ära sega“ olekuga mittearvestamine, jne) + Kohenda teavituste seadistusi + Helimärguanded, „Ära sega“ olekuga mittearvestamine, jne. + Jätka pidevate märguannetega + Anna märku vaid üks kord diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index a87c2a9c..5449bf84 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -14,7 +14,6 @@ گزارش یک نقص فنی مطالعه مستندات رتبه دهی به اپ ⭐ - حمایت مالی 💸 %1$d اطلاعیه %1$d اطلاعیه در حال اتصال دوباره … diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index dad7df0a..25750d54 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -60,7 +60,7 @@ Tilattu kahteen topikkiin Tilausasetukset Tallennettu nimellä %1$s lataukset kansioon - jälkeen kolmenkymmenen päivän + Kuukauden jälkeen Oletus Lähetä testi ilmoitus korkea @@ -91,14 +91,14 @@ Tilaus palvelu Tilattu viiteen välittömään topikkiin Lisää käyttäjiä - jälkeen kolmen kuukauden + Kolmen kuukauden jälkeen Lisää tilaus Ilmoitukset hiljennetty Näytä kaikki ilmoitukset %1$s \ntiedosto: %2$s, lataus virhe Poista pysyvästi - Jälkeen kolmen päivän + Kolmen päivän jälkeen Vaalea tila päälle Ei voida avata liitettä %1$s Jos tiedoston koko on alle 5 MB @@ -147,7 +147,7 @@ Kaikki Lisää/poista käyttäjiä suojatuille topikeille Syötä palvelun URL-osoitte alle tilataksesi topikkeja muilta palvelimilta. - Jälkeen yhden päivän + Päivän jälkeen Tumma tila päällä. Oletko vampyyri \? Lisää uusi käyttäjä uudelle palvelimelle Käytä oletusasetusta @@ -291,7 +291,7 @@ %1$s \nTiedosto: %2$s Ilmoitukset mykistetty, kunnes niitä jatketaan - Esimerkki (käytä curl):

$ curl -d \"Hei\" %1$s
+ $ curl -d "Hei" %1$s ]]> Kuvake Välitön lähetys pois Tilattu neljään välittömään topikkiin @@ -314,7 +314,7 @@ Lataa automaattisesti liitteet Muokkaa käyttäjää Palautus onnistui - jälkeen seitsemän päivän + Viikon jälkeen Palautus epäonnistui: %1$s Ilmoitukset Tämä on testi-ilmoitus ntfy Android -sovelluksesta. Sillä on prioriteettitaso %1$d. Jos lähetät toisen, se voi näyttää erilaiselta. @@ -328,8 +328,7 @@ Nauhoitetut logit Pidä hälytykset korkeimmalla tasolla Vain maksimi ja ylittävät - About - Lahjoita 💸 + Tietoja 8 tuntia Topikki %1$s tilattu Käyttäjätunnus @@ -340,4 +339,7 @@ %1$s (Yleis Push) Tagit %1$s %1$d Huomautus + Kysy myöhemmin + Sulje + Salli nyt diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3b802362..673e78e5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -329,7 +329,6 @@ URL du sujet Défaut Conserver les notifications - Faire un don 💸 Les applications ne peuvent plus être installées. Veuillez les télécharger via un navigateur. Voir le ticket #531 pour plus de détails. Conserver les notifications avec une priorité maximale Paramètres personnalisés de la notification diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 0b46baac..b35e1473 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -31,7 +31,6 @@ Informar dun fallo Ler documentación Valorar a app ⭐ - Doar 💸 Retirar subscrición Retirar a subscrición ao(s) asunto(s) seleccionado(s) e eliminar definitivamente tódalas notificacións\? Eliminar definitivamente diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e889a626..2f270648 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -79,7 +79,6 @@ विषय की सदस्यता लें पीछे जाएं एक विषय की सदस्यता - दान करें %1$d सूचना रद्द करें रद्द करें diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index e7536971..b6a77b4a 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -80,7 +80,6 @@ ponovno povezivanje… Uključi sada Prebacivanje na WebSockete je preporučen način povezivanja sa serverom, i moglo bi poboljšati životni vijek baterije, ali može zahtjevati dodatnu konfiguraciju u tvojem proxy-u. Ovo možeš promijeniti u postavkama. - Doniraj 💸 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. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 4c8df922..752ce0ef 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -50,7 +50,6 @@ %1$d értesítés Mégse Alapértelmezett - Adomány 💸 Bezár Garantálja az azonnali üzenetküldést, akkor is, ha az eszköz inaktív. Az azonnali üzenetküldés mindig bekapcsolva a %1$s címen kívül. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 4c0d90da..f68893c4 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -55,7 +55,7 @@ Pengguna baru Anda belum menerima notifikasi apa pun. Untuk mengirimkan notifikasi ke topik ini, lakukan PUT atau POST ke URL topik. - Contoh (menggunakan curl):

$ curl -d \"Hai\" %1$s
+ $ curl -d \"Hi\" %1$s ]]> Instruksi rinci tersedia di ntfy.sh, dan dalam dokumentasi. Hapus semua notifikasi di topik ini\? Hapus secara permanen @@ -327,7 +327,6 @@ Disalin ke papan klip Tentang URL Topik - Donasi 💸 Aplikasi tidak dapat dipasang lagi. Unduh melalui peramban. Lihat masalah #531 untuk detail lebih lanjut. Notifikasi prioritas maks hanya memperingati sekali Atur pengaturan notifikasi @@ -343,4 +342,11 @@ ntfy akan menjadi sebagai distributor UnifiedPush ntfy tidak akan menjadi sebagai distributor UnifiedPush Aktifkan UnifiedPush + Untuk memastikan WebSockets tersambung kembali di latar belakang, berikan izin Alarm & Pengingat untuk ntfy + Tanyakan nanti + Singkirkan + Berikan sekarang + Alarm akurat + ntfy dapat menjadwalkan alarm yang tepat. Alarm yang tepat diperlukan untuk menyambungkan kembali WebSockets di latar belakang. Klik untuk mencabut izin. + ntfy tidak dapat menjadwalkan alarm yang tepat. Alarm yang tepat diperlukan untuk menyambungkan kembali WebSockets di latar belakang. Klik untuk memberikan izin. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7bc980b4..3488a85e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,18 +1,18 @@ Priorità bassa - Iscritto a tre topic + Iscritto a tre argomenti Priorità alta Priorità massima Servizio di iscrizione - Iscritto ai topic a consegna istantanea - Iscritto a due topic a consegna istantanea - Iscritto a tre topic a consegna istantanea - Iscritto a quattro topic a consegna istantanea - Iscritto a %1$d topic a consegna istantanea - Iscritto ai topic - Iscritto a un topic - Iscritto a due topic + Iscritto ad argomenti a consegna istantanea + Iscritto a due argomenti a consegna istantanea + Iscritto a tre argomenti a consegna istantanea + Iscritto a quattro argomenti a consegna istantanea + Iscritto a %1$d argomenti a consegna istantanea + Iscritto agli argomenti + Iscritto ad un argomento + Iscritto a due argomenti %1$d notifiche ricevute Tutto è aggiornato Impossibile aggiornare %1$d iscrizioni @@ -23,73 +23,73 @@ Notifiche disattivate Notiche disattivate fino a %1$s Impostazioni - Topic iscritti - Disiscriversi dai topic selezionati ed eliminare definitivamente tutte le notifiche\? + Argomenti sottoscritti + Disiscriversi dagli argomenti selezionati ed eliminare definitivamente tutte le notifiche? Elimina definitivamente %1$d notifiche %1$d notifiche riconnessione… ieri %1$s (UnifiedPush) - Istruzioni dettagliate disponibili su ntfy.sh, e nella documentazione. + Istruzioni dettagliate disponibili su ntfy.sh e nella documentazione. Questa iscrizione è gestita da %1$s via UnifiedPush Chiedi in seguito Abbandona Correggi ora Chiedi in seguito Abbandona - Iscriviti al topic - Nome del topic, es. phils_alerts + Iscriviti all\'argomento + Nome dell\'argomento, es. phils_alerts Usa un altro server - Immettere gli URL dei servizi qui sotto per iscriversi ai topic di altri server. + Inserisci gli URL dei servizi qui sotto per iscriversi agli argomenti di altri server. Consegna istantanea in modalità doze - La consegna istantanea è sempre attiva per gli host diversi da %1$s. - Cancella - Log in + La consegna istantanea è sempre attiva per i sistemi diversi da %1$s. + Annulla + Accesso Connessione fallita: %1$s - Login richiesto - Username + Accesso richiesto + Nome utente Password - Login fallito. Utente %1$s non autorizzato. + Accesso fallito. Utente %1$s non autorizzato. Nuovo utente - Per inviare notifiche a questo topic, usa PUT o POST all\'URL del topic. - Istruzioni dettagliate disponibili su ntfy.sh, e nella documentazione. - Eliminare tutte le notifiche in questo topic\? + Per inviare notifiche a questo argomento, usa PUT o POST all\'URL dell\'argomento. + Istruzioni dettagliate disponibili su ntfy.sh e nella documentazione. + Eliminare tutte le notifiche in questo argomento? Elimina definitivamente - Cancella - Disiscriversi da questo topic e cancellare tutte le notifiche ricevute\? + Annulla + Disiscriversi da questo argomento ed eliminare tutte le notifiche ricevute? Elimina definitivamente - Cancella + Annulla Test: Puoi impostare un titolo, se vuoi. Impossibile inviare il messaggio: Pubblicazione anonima non permessa. Impossibile inviare il messaggio: L\'utente \"%1$s\" non è autorizzato. Copiato negli appunti - Consegna istantanea ON - Tags: %1$s + Consegna istantanea ATTIVATA + Etichette: %1$s Impossibile inviare il messaggio: L\'allegato è troppo grande. Notifica eliminata Annulla Notifica copiata negli appunti - Impossibile aprire o scaricare l\'allegato. Il link è scaduto e nessun file locale è stato trovato. + Impossibile aprire o scaricare l\'allegato. Il collegamento è scaduto e nessun file locale è stato trovato. Impossibile aprire l\'allegato: %1$s Impossibile aprire URL: %1$s Impossibile eliminare l\'allegato: %1$s Impossibile scaricare l\'allegato: %1$s non scaricato - non scaricato, link scaduto + non scaricato, collegamento scaduto non scaricato, scadenza %1$s %1$d%% scaricato eliminato - eliminato, link scaduto - eliminato, scadenza link %1$s + eliminato, collegamento scaduto + eliminato, scadenza collegamento %1$s download fallito - download fallito, link scaduto + download fallito, collegamento scaduto Notifiche disattivate Notifiche disattivate fino a %1$s Abilita consegna istantanea Disabilita consegna istantanea Invia notifica di test - download fallito, scadenza link %1$s + download fallito, scadenza collegamento %1$s Cancella tutte le notifiche Elimina Eliminare definitivamente le notifiche selezionate\? @@ -103,7 +103,7 @@ Impossibile leggere l\'immagine: %1$s Condividi con Messaggio pubblicato - Cancella + Annulla Mostra tutte le notifiche 1 ora Fino a domani @@ -127,7 +127,7 @@ Mostra notifiche se la priorità è 5 (max) Tutte le priorità Priorità bassa e superiori - Priorità di default e superiori + Priorità predefinita e superiori Priorità alta e superiori Solo priorità massima Scarica automaticamente tutti gli allegati @@ -137,78 +137,74 @@ Se sotto 100kB Se sotto 500 kB Elimina tutte le notifiche - Server di default + Server predefinito Generale - %1$s (default) + %1$s (predefinito) Gestisci utenti - Aggiungi/rimuovi utenti per topic protetti + Aggiungi/rimuovi utenti per argomenti protetti Utenti - Non utilizzato da nessun topic - Utilizzato dal topic %1$s + Non utilizzato da nessun argomento + Utilizzato dall\'argomento %1$s Aggiungi nuovo utente Crea un nuovo utente per un nuovo server - Modalità dark - Utilizzando il default di sistema - Modalità light ON - Usa il default di sistema - Modalità light + Modalità scura + Utilizzo impostazione predefinita di sistema + Modalità chiara attiva + Usa impostazione predefinita di sistema + Modalità chiara Solo le impostazioni Messaggi broadcast Le app possono ricevere le notifiche in ingresso come broadcast Abilita registrazione dei log Log caricati e URL copiato Impossibile caricare i log: %1$s - Nessun topic/hostname è stato redatto. Forse non hai iscrizioni\? - Questi topic/hostnames sono stati sostituiti con nomi di frutta, così puoi condividere i log senza preoccupazioni: -\n -\n%1$s -\n -\nLe password sono state ripulite, ma non sono elencate qui. + Nessun argomento/nome di sistema è stato redatto. Forse non hai iscrizioni? + Questi argomenti/nomi di sistema sono stati sostituiti con nomi di frutta, così puoi condividere i log senza preoccupazioni: \n \n%1$s \n \nLe password sono state ripulite, ma non sono elencate qui. Ok - Cancella i log + Cancella i registri Usa stream JSON over HTTP per collegarti al server. Questo metodo è collaudato, ma può consumare più batteria. Stream JSON over HTTP - Puoi aggiungere un utente qui. Tutti i topic per il dato server utilizzeranno questo utente. - Puoi modificare username/password per l\'utente selezionato, oppure eliminarlo. + Puoi aggiungere un utente qui. Tutti gli argomenti per il server specificato utilizzeranno questo utente. + Puoi modificare nome utente/password per l\'utente selezionato, oppure eliminarlo. URL del servizio - Username + Nome utente Password Password (non modificata se il campo viene lasciato vuoto) Aggiungi utente - Cancella + Annulla Elimina utente Salva - Iscritto a %1$d topic - Priorità di default + Iscritto a %1$d argomenti + Priorità predefinita Priorità minima - Iscritto a un topic a consegna istantanea - Iscritto a quattro topic + Iscritto ad un argomento a consegna istantanea + Iscritto a quattro argomenti Leggi la documentazione Disiscriviti - Clicca + per creare o iscriversi ad un topic. In seguito, riceverai notifiche sul tuo device quando invierai messaggi via PUT o POST. + Clicca + per creare o iscriversi ad un argomento. In seguito, riceverai notifiche sul tuo dispositivo quando invierai messaggi via PUT o POST. L\'ottimizzazione della batteria deve essere disabilitata per l\'app per evitare problemi di consegna delle notifiche. Aggiungi iscrizione - Cancella + Annulla Sembra che non ci sia nessuna iscrizione al momento. - Segnala un bug + Segnala un problema Valuta l\'app ⭐ - I topic possono essere non protetti da password, per cui scegli un nome che è difficile da indovinare. Una volta iscritti, è possibile effettuare notifiche PUT/POST. + Gli argomenti possono essere non protetti da password, per cui scegli un nome che sia difficile da indovinare. Una volta iscritto, è possibile effettuare notifiche PUT/POST. Assicura che i messaggi siano consegnati immediatamente, anche se il device non è attivo. - Questo topic richiede il login. Per favore, inserire username e password. + Questo argomento richiede l\'accesso. Per favore, inserisci nome utente e password. Iscriviti Indietro - Non hai ancora ricevuto notifiche su questo topic. + Non hai ancora ricevuto notifiche su questo argomento. Apri file $ curl -d \"Hi\" %1$s ]]> - Consegna istantanea OFF + Consegna istantanea DISATTIVATA Elimina file Salva il file Copia URL - Copia l\'indirizzo del topic + Copia l\'indirizzo dell\'argomento URL copiato negli appunti Notifiche ON Impossibile inviare il messaggio: %1$s - Interrompi il download + Annulla download Copia notifica Salvato con nome \"%1$s\" nella cartella \"Downloads\" Impossibile salvare l\'allegato: %1$s @@ -230,21 +226,21 @@ Scarica allegati Scarica file Scarica - Cancella + Annulla Scarica tutto automaticamente Se sotto 1 MB Se sotto 10 MB Se sotto 50 MB - Impossibile aprire l\'allegato: Il file può essere stato cancellato oppure nessuna app installata è in grado di aprire il file. + Impossibile aprire l\'allegato: Il file può essere stato eliminato oppure nessuna app installata è in grado di aprire il file. Disiscriviti - Cancella + Annulla Notifiche Impossibile leggere le informazioni del file: %1$s Apri Notifiche disattivate fino al ripristino Se sotto 5 MB Un file è stato condiviso con te - Topic suggeriti + Argomenti suggeriti Disattivare le notifiche Salva Notifiche ripristinate @@ -255,34 +251,34 @@ 8 ore Tutto, eccetto utenti Avanzate - Inserisci il root URL del tuo server per utilizzarlo come default durante l\'iscrizione a nuovi topic e/o durante la condivisione ai topic. - Utilizzato dai topic %1$s + Inserisci l\'URL radice del tuo server per utilizzare il tuo server come predefinito quando ti iscrivi a nuovi argomenti e/o condividi argomenti. + Utilizzato dagli argomenti %1$s Aggiungi utente - Modalità dark + Modalità scura Backup su file - Modalità dark ON. Sei un vampiro\? + Modalità scura attiva. Sei un vampiro? Backup & Ripristino Ripristino fallito: %1$s Log copiati negli appunti Informazioni - Esporta config, notifiche e utenti + Esporta configurazione, notifiche e utenti Tutto Backup creato Backup fallito: %1$s Ripristina da file - Importa config, notifiche e utenti - Logging (fino a 1,000 elementi) nel device … + Importa configurazione, notifiche e utenti + Registrazione (fino a 1.000 voci) sul dispositivo … Ripristino riuscito Le app non possono ricevere le notifiche come broadcast - Carica e copia link - Attiva la trascrizione dei log per condividere file di log in seguito per diagnosticare problemi. + Carica e copia collegamento + Attiva la registrazione, così potrai condividere i registri in un secondo momento per diagnosticare i problemi. Copia negli appunti (censurato) Versione ntfy %1$s (%2$s) Copia/upload file di log Copia negli appunti - Carica e copia link (censurato) - Log in upload … + Carica e copia collegamento (censurato) + Caricando registri … Elimina i log precedentemente salvati, e ricomincia Protocollo di connessione Usa WebSockets per collegarti al server. Questo è il metodo consigliato, ma potrebbe richiedere una configurazione aggiuntiva del proxy. @@ -292,19 +288,19 @@ Modifica utente In attesa di notifiche in ingresso Questa è una notifica test dall\'app Android ntfy. Ha livello di priorità %1$d. Se ne invii un\'altra, potrebbe avere contenuti differenti. - Copia i log negli appunti, o carica su nopaste.net (in possesso dell\'autore di ntfy). Hostname e topic possono essere censurati, le notifiche non lo saranno mai. - default + Copia i log negli appunti, o carica su nopaste.net (in possesso dell\'autore di ntfy). I nomi di sistema e gli argomenti possono essere censurati, le notifiche non lo saranno mai. + predefinita massima alta minima bassa - Iscritto al topic %1$s + Iscritto all\'argomento %1$s Utilizzare l\'impostazione globale - Esclusione del DND (Do Not Disturb), suoni, ecc. - Iscritto a cinque topic a consegna istantanea - Iscritto a sei topic a consegna istantanea - Iscritto a cinque topic - Iscritto a sei topic + Disattivazione funzione Non disturbare (DND), suoni, ecc. + Iscritto a cinque argomenti a consegna istantanea + Iscritto a sei argomenti a consegna istantanea + Iscritto a cinque argomenti + Iscritto a sei argomenti %1$s fallito: %2$s Impostazioni del canale Consegna istantanea @@ -314,7 +310,7 @@ Icona della sottoscrizione Impostare un\'icona da visualizzare nelle notifiche Icona della sottoscrizione (toccare per rimuovere) - Icona visualizzata nelle notifiche di questo topic + Icona visualizzata nelle notifiche di questo argomento Impossibile salvare l\'icona: %1$s utilizzando l\'impostazione globale Nome visualizzato @@ -327,18 +323,17 @@ Scegli il servizio URL Pulisci il servizio URL Attiva ora - Default - Dona 💸 - Le app non possono più essere installate: devono essere scaricate via browser. Vedi l\'issue #531 per dettagli. + Predefinita + Le app non possono più essere installate: devono essere scaricate tramite browser. Vedi la segnalazione #531 per dettagli. Mantieni l\'alert per le notifiche a priorità massima Attiva UnifiedPush - Bypass Do-Not-Disturb, suoni, etc. + Esclusione Non disturbare (DND), suoni, ecc. Continua ad inviare notifiche ntfy non si comporterà come un distributore UnifiedPus ntfy si comporterà come un distributore UnifiedPush Impostazioni di notifica personalizzate Usa impostazioni personalizzate per questa iscrizione - Impostazioni predefinite sono in uso (suoni, bypass Do-Not-Disturb, etc.) + Utilizzo delle impostazioni predefinite (suoni, esclusione della funzione Non disturbare, ecc.) Configura le impostazioni di notifica Invia notifiche solo una volta Le notifiche a massima priorità continuano ad allertare fino a che non vengono rimosse diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 663f2b18..e2320755 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -39,7 +39,6 @@ התחברות מחדש… לחיצה על + תאפשר ליצור או להירשם לנושא. לאחר מכן התראות תגענה למכשיר שלך בעת שליחת הודעות עם PUT או POST. הוראות מפורטות זמינות ב־ntfy.sh, ובתיעוד. - תרומה 💸 מנוי לשני נושאים במסירה מיידית מנוי לשלושה נושאים במסירה מיידית מנוי לנושאים במסירה מיידית diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 605201ab..30543449 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -327,7 +327,6 @@ About トピックのURL クリップボードにコピーしました - 寄付する💸 アプリはインストールできなくなりました。代替手段としてブラウザからダウンロードしてください。詳細は issue #531 をご参照ください。 優先度最高は非表示になるまで通知継続 デフォルト diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 5e9b81dd..076bcf77 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,13 +1,13 @@ 자세한 설명은 ntfy.sh와 docs 페이지에서 찾으실 수 있습니다. - 알림 (우선순위 높음) + 우선순위 높음 알림 음소거됨 즉시 전달 주제 3개 구독중 다른 서버 사용 알림 켜짐 - 알림 (우선순위 기본) - 알림 (우선순위 최상) + 우선순위 기본 + 우선순위 최상 구독 서비스 알림 수신중 즉시 전달 주제를 구독함 @@ -165,9 +165,9 @@ 연결 프로토콜 JSON stream over HTTP 표시 설정 - 알림 (우선순위 낮음) + 우선순위 낮음 저장 - 알림 (우선순위 최하) + 우선순위 최하 즉시 전달 주제 2개 구독중 즉시 전달 주제 5개 구독중 즉시 전달 주제 4개 구독중 @@ -214,7 +214,7 @@ 파일 열기 URL이 클립보드에 복사됨 서비스 URL 선택 - 예제 (curl 사용):

$ curl -d \\\"Hi\\\" %1$s
+ 예제 (curl 사용):
$ curl -d \\\"Hi\\\" %1$s
파일 정보를 읽을 수 없습니다: %1$s 테스트: 원한다면 제목을 설정할 수 있습니다. 즉시 전달 꺼짐 @@ -328,4 +328,5 @@ 사용자 추가 사용자 삭제 이 구독에 사용자 설정 사용 + 기본 그룹
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml new file mode 100644 index 00000000..61f3bd04 --- /dev/null +++ b/app/src/main/res/values-mk/strings.xml @@ -0,0 +1,8 @@ + + + Низок приоритет + Мин приоритет + Стандарден приоритет + Висок приоритет + Највисок приоритет + diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 7de6eda0..3f71002a 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -34,7 +34,6 @@ Notifikasi disenyapkan hingga %1$s Tetapan Aduan Kerosakan - Sumbangan 💸 Padamkan secara kekal Batal %1$d notifikasi diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 628222e9..a51cb51e 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -324,7 +324,6 @@ %1$s (standard) Kopier til utklippstavlen Angi et tilpasset visningsnavn for dette abonnementet. La stå tomt for standard (%1$s). - Doner 💸 Å bytte til WebSockets er den anbefalte måten å koble til serveren på, og kan forbedre batterilevetiden, men kan kreve ytterligere konfigurasjon i proxy-serveren. Dette kan endres i innstillingene. Aktiver nå Abonnerte på emnet %1$s diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 00000000..88bf71d7 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,147 @@ + + + #84D6C2 + #00382E + #005144 + #A0F2DD + #B1CCC4 + #1D352F + #334B45 + #CDE8DF + #AACBE4 + #113447 + #2A4A5F + #C8E6FF + #FFB4AB + #690005 + #93000A + #FFDAD6 + #121212 + #E0E0E0 + #121212 + #E0E0E0 + #3F4946 + #C0C0C0 + #89938F + #3F4946 + #000000 + #E0E0E0 + #2B3230 + #076B5B + #A0F2DD + #00201A + #84D6C2 + #005144 + #CDE8DF + #06201A + #B1CCC4 + #334B45 + #C8E6FF + #001E2E + #AACBE4 + #2A4A5F + #121212 + #383838 + #0D0D0D + #1B1B1B + #1B2023 + #282F33 + #333333 + #9AECD7 + #002C24 + #4D9F8C + #000000 + #C7E2D9 + #112A24 + #7C968E + #000000 + #C0E1FA + #02293C + #7595AC + #000000 + #FFD2CC + #540003 + #FF5449 + #000000 + #0E1513 + #DEE4E0 + #0E1513 + #FFFFFF + #3F4946 + #D4DFDA + #AAB4B0 + #88938F + #000000 + #DEE4E0 + #252B29 + #005245 + #A0F2DD + #001510 + #84D6C2 + #003E34 + #CDE8DF + #001510 + #B1CCC4 + #233B35 + #C8E6FF + #00131F + #AACBE4 + #18394E + #0E1513 + #3F4643 + #040807 + #191F1D + #232927 + #2D3432 + #393F3D + #B2FFEB + #000000 + #81D2BE + #000E0A + #DAF6ED + #000000 + #ADC8C0 + #000E0A + #E3F2FF + #000000 + #A6C7E0 + #000D17 + #FFECE9 + #000000 + #FFAEA4 + #220001 + #0E1513 + #DEE4E0 + #0E1513 + #FFFFFF + #3F4946 + #FFFFFF + #E8F2EE + #BBC5C1 + #000000 + #DEE4E0 + #000000 + #005245 + #A0F2DD + #000000 + #84D6C2 + #001510 + #CDE8DF + #000000 + #B1CCC4 + #001510 + #C8E6FF + #000000 + #AACBE4 + #00131F + #0E1513 + #4B514F + #000000 + #1B211F + #2B3230 + #363D3A + #424846 + + #1B2023 + #121212 + diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 9c6eb815..00000000 --- a/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..9d0d8644 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 28fc303b..6d3b1248 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -328,7 +328,6 @@ Nu inschakelen WebSockets is de aangeraden manier om te verbinden met uw server en kan batterij verbruik verminderen. Het kan extra configuratie in uw proxy vereisen. Dit kan omgeschakeld worden in de instellingen. Standaard - Doneer 💸 Apps kunnen niet meer worden geïnstalleerd. Download deze via de browser. Raadpleeg issue #531 voor meer details. Behoud meldingen voor hoogste prioriteit Max prioriteit berichten geven continue een melding totdat deze worden gesloten diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c2b2bfa2..27cd59fd 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -327,7 +327,6 @@ Odrzuć Aktywuj teraz Użyj strumienia JSON przez HTTP, aby połączyć się z serwerem. Ta metoda jest sprawdzona, ale może zużywać więcej baterii. - Wspomóż💸 Aplikacja nie może zostać zainstalowana. Pobierz ją poprzez przeglądarkę. Sprawdź problem #531 po więcej informacji. Nadal wysyłaj powiadomienia dla najwyższych priorytetów Własne ustawienia powiadomień diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 84950653..55385e8b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -324,7 +324,6 @@ Limpar URL do serviço Habilitar agora Padrão - Doar 💸 As notificações de prioridade máxima alertam apenas uma vez Configurar configurações de notificação Ignorar o Não Perturbe, sons, etc. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b87ded12..21e741df 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -219,7 +219,7 @@ Esse tópico necessita autenticação. Por favor, insira um nome de utilizador e palavra-passe. Novo utilizador Escolha URL de serviço - Exemplo (utilizando curl):

$ curl -d \"Olá\" %1$s
+ $ curl -d \"Olá\" %1$s ]]> Deseja anular a subscrição deste tópico e eliminar todas as notificações recebidas\? Esta é uma notificação de teste da aplicação Android do ntfy. Tem uma prioridade de nível %1$d. Se enviar outra notificação, poderá ser diferente. Não foi possível enviar a mensagem: O anexo é grande demais. @@ -258,7 +258,6 @@ Modo claro Modo escuro Usar a predefinição do sistema - Doar 💸 Falha ao restaurar %1$s Avançado Messagens de transmissão @@ -343,4 +342,11 @@ Habilitar UnifiedPush ntfy atuará como distribuidora UnifiedPush ntfy não atuará como distribuidora UnifiedPush + Para garantir que os WebSockets se reconectem em segundo plano, conceda a permissão de Alarmes e Lembretes à aplicação ntfy + Lembrar mais tarde + Ignorar + Autorizar agora + 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.
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index a20d2f1e..5970a63d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -162,7 +162,6 @@ Raportează o problemă Citește documentația Dezabonează-te de la topicele selectate și șterge notificările permanent\? - Donează 💸 %1$s (UnifiedPush) Adaugă abonament %1$d notificări @@ -313,7 +312,7 @@ Logare eșuată. Utilizatorul %1$s nu este autorizat. Utilizator nou Nu ai primit încă notificări pentru acest topic. - Exemplu (folosing curl):

$ curl -d \"Salut\" %1$s
+ Exemplu (folosing curl):
$ curl -d \"Salut\" %1$s
Anulează Test: Poți specifica un titlu dacă dorești. Această este o notificare de test de la aplicația ntfy pentru Android. Are nivelul priorității %1$d. Dacă trimiți altă notificare, s-ar putea să arate altfel. @@ -343,4 +342,9 @@ ntfy va avea rol de distribuitor UnifiedPush Activează UnifiedPush ntfy nu va avea rol de distribuitor UnifiedPush + Întreabă mai târziu + Respinge + 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. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 24d6ae32..55f11199 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -18,7 +18,7 @@ Удалить навсегда Отмена Тест: Вы можете установить заголовок, если хотите. - Пример (используя curl):

$ curl -d \"Привет\" %1$s
+ $ curl -d \"Привет\" %1$s ]]> Удалить все уведомления в этой теме\? Удалить навсегда Не получилось отправить сообщение: %1$s @@ -313,7 +313,6 @@ Установка приложений через уведомления больше не поддерживается. Скачайте приложение через браузер. Подробности смотрите в отчёте ntfy #531. Иконка подписки Использовать иконку для отображения в уведомлениях - Пожертвовать 💸 Уведомления с наивысшим приоритетом будут давать о себе знать только один раз Уведомления будут доставляться с помощью Firebase. Могут быть задержки с доставкой, но потребляется меньше энергии. Очистить URL-адрес сервера @@ -343,4 +342,11 @@ Используются пользовательские настройки для этой подписки Уведомлять только один раз Продолжать уведомлять при наивысшем приоритете + Чтобы WebSockets работал в фоновом режиме, проставьте ntfy права «Будильники и напоминания» + Спроси потом + Закрыть + Предоставить + Точные оповещения + ntfy может выставлять точные оповещения. Точные оповещения необходимы для переподключения WebSockets в фоновом режиме. Нажмите, чтобы отозвать разрешение. + ntfy может выставлять точные оповещения. Точные оповещения необходимы для переподключения WebSockets в фоновом режиме. Нажмите, чтобы дать разрешение. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a6746b70..251ce74e 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -282,7 +282,6 @@ Ohodnotiť aplikáciu ⭐ Prečítať dokumentáciu Oznámenia stlmené do %1$s - Prispieť 💸 Odhlásiť odber z vybraných tém a natrvalo vymazať všetky oznámenia\? Vymazať natrvalo Odhlásiť odber diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 9005c50c..ec45fbac 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -70,7 +70,7 @@ Rensa tjänstens URL Du har inte fått några meddelanden för detta ämne ännu. För att skicka meddelanden till det här ämnet, PUT eller POST till ämnesadressen. - Exempel (med curl):

$ curl -d \"Hej\" %1$s
+ $ curl -d \"Hej\" %1$s ]]> Detaljerade instruktioner finns på ntfy.sh och i dokumentationen. Ta bort alla meddelanden i det här ämnet\? Ta bort permanent @@ -113,7 +113,6 @@ Prenumerationsinställningar Dela Dela - Donera 💸 Ångra Öppna fil Ta bort fil @@ -343,4 +342,11 @@ Meddela endast en gång Använd de globala inställningarna Ikon som visas i meddelanden för detta ämne + För att säkerställa att WebSockets återansluter i bakgrunden, bevilja behörigheten Alarm & Påminnelser till ntfy + Fråga senare + Avfärda + Bevilja nu + Exakta larm + ntfy kan schemalägga exakta larm. Exakta larm krävs för att återansluta WebSockets i bakgrunden. Klicka för att återkalla behörigheten. + ntfy kan inte schemalägga exakta larm. Exakta larm krävs för att återansluta WebSockets i bakgrunden. Klicka för att bevilja behörigheten. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 8bd3a285..7d64b6b6 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -40,7 +40,6 @@ ஒரு பிழையைப் புகாரளிக்கவும் ஆவணத்தைப் படியுங்கள் பயன்பாட்டை மதிப்பிடுங்கள் - நன்கொடை குழுவிலகவும் தேர்ந்தெடுக்கப்பட்ட தலைப்பு (கள்) இலிருந்து குழுவிலகவும், அனைத்து அறிவிப்புகளையும் நிரந்தரமாக நீக்கவா? ரத்துசெய் diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 93daccb4..b65f8038 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -35,7 +35,6 @@ รายงานปัญหา(Bug) อ่านเอกสาร ให้คะแนนแอป ⭐ - บริจาค 💸 ยกเลิกการสมัครรับ ต้องการยกเลิกการสมัครจากหัวข้อที่เลือกและลบการแจ้งเตือนทั้งหมดอย่างถาวรใช่ไหม ลบถาวร diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d25807ce..550042ab 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -19,9 +19,7 @@ Her şey güncel Bildirimler şu zamana kadar sessize alındı: %1$s %1$d konuya abone olundu - %1$d abonelik yenilenemedi -\n -\n%2$s + %1$d abonelik yenilenemedi\n\n%2$s Abonelik yenilenemedi: %1$s Abone olunan konular Bildirimler açık @@ -209,7 +207,7 @@ dün Kapat Şimdi düzelt - Örnek (curl kullanarak):

$ curl -d \"Merhaba\" %1$s
+ $ curl -d "Merhaba" %1$s ]]> Kalıcı olarak sil Mesaj gönderilemiyor: \"%1$s\" kullanıcısı yetkilendirilmedi. Ayrıntılı talimatlar ntfy.sh adrsimde ve belgelerde bulunabilir. @@ -327,7 +325,6 @@ Hakkında Konu URL\'si Panoya kopyalandı - Bağış yap 💸 Uygulamalar artık kurulamıyor. Bunun yerine tarayıcı üzerinden indirin. Ayrıntılar için sorun #531\'e bakın. En yüksek öncelikli bildirimler yalnızca bir kez uyarı verir Bu abonelik için özel ayarları kullan @@ -343,4 +340,11 @@ UnifiedPush\'u etkinleştir ntfy bir UnifiedPush dağıtıcısı olarak davranmayacaktır ntfy bir UnifiedPush dağıtıcısı olarak davranacaktır + WebSocket bağlantılarının arka planda yeniden bağlanmasını sağlamak için ntfy uygulamasına Alarm ve Hatırlatıcılar iznini verin + Daha Sonra Hatırlat + Yoksay + Şimdi izin ver + Kesin alarmlar + ntfy, kesin alarmlar planlayabilir. Kesin alarmlar, WebSocket’lerin arka planda yeniden bağlanması için gereklidir. İzni geri almak için tıklayın. + ntfy, kesin alarmlar planlayamaz. Kesin alarmlar, WebSocket’lerin arka planda yeniden bağlanması için gereklidir. İzni vermek için tıklayın. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5c1cd674..e78b28b0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -331,7 +331,6 @@ Використання налаштувань за замовчуванням (звуки, \"Не турбувати\" тощо) Перевизначення режиму \"Не турбувати\" (DND), звуки тощо. Продовжувати сповіщати - Пожертвувати 💸 Безперервне сповіщення для найвищого пріоритету Сповіщення з максимальним пріоритетом безперервно сповіщають, доки не будуть закриті Сповіщення з максимальним пріоритетом сповіщають лише один раз diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index d9b6728f..f27ec919 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -248,7 +248,6 @@ Uchta darhol yuborish mavzulariga obuna bo‘ldik Bildirishnomalar %1$s gacha o‘chirilgan Beshta mavzuga obuna bo‘ldik - Xayriya qiling 💸 Bekor qilish Boshqa serverlardan mavzularga obuna bo‘lish uchun quyida URL manzillarini kiriting. Ushbu mavzu tizimga kirishni talab qiladi. Iltimos, foydalanuvchi nomi va parolni kiriting. diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index d8d77671..4b9beb3a 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -25,7 +25,6 @@ Chờ thông báo Đã nhận %1$d thông báo Xóa vĩnh viễn - Quyên góp 💸 Hủy Hủy đăng kí các chủ đề đã chọn và xóa tất cả thông báo? Hỏi sau diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index aeb279ac..cff8a815 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -327,7 +327,6 @@ 关于 主题 URL 已复制到剪贴板 - 捐赠 💸 无法再安装应用。 请通过浏览器下载。 有关详细信息,请参阅问题 #531。 默认 持续以最高优先级通知进行提醒,直至取消 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f41db28f..316205d8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -108,7 +108,6 @@ 即時通知 使用全域設定 新增使用者 - 捐獻 💸 復原 已下載 %1$d%% 啓用即時通知 @@ -213,7 +212,7 @@ 儲存為 \"Downloads\" 資料中的 \"%1$s\" 不能夠發布信息:用戶 %1$s 不被授權。 PUT 或 POST 主題網址以傳送通訊。 - 例如(使用 curl):

$ curl -d \"Hi\" %1$s
+ $ curl -d "Hi" %1$s]]> 不能夠傳送訊息:%1$s 標籤:%1$s 啟動即時傳送 @@ -343,4 +342,11 @@ 無法保存圖標:%1$s 使用全局設置 您可以編輯該用戶的用戶名和密碼,或刪除該用戶。 + 為確保 WebSocket 能在背景重新連線,請授予 ntfy「鬧鐘與提醒」權限 + 關閉 + 稍後再問 + 立即授權 + 精準提醒 + ntfy 可以排程精準提醒。精準提醒是讓 WebSocket 能在背景重新連線的必要條件。點擊以撤銷此權限。 + ntfy 無法排程精準提醒。精準提醒是讓 WebSocket 能在背景重新連線的必要條件。點擊以授予此權限。 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 00ae9757..6a78d973 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,17 +1,151 @@ - - - #ff000000 - #121212 - #1b2023 - #282F33 - #dddddd - #eeeeee - #ffffffff + + + #338574 + #FFFFFF + #A0F2DD + #005144 + #4B635C + #FFFFFF + #CDE8DF + #334B45 + #436278 + #FFFFFF + #C8E6FF + #2A4A5F + #BA1A1A + #FFFFFF + #FFDAD6 + #93000A + #FFFFFF + #171D1B + #FFFFFF + #171D1B + #E0E0E0 + #3F4946 + #6F7976 + #C0C0C0 + #000000 + #2B3230 + #F0F0F0 + #84D6C2 + #A0F2DD + #00201A + #84D6C2 + #005144 + #CDE8DF + #06201A + #B1CCC4 + #334B45 + #C8E6FF + #001E2E + #AACBE4 + #2A4A5F + #E0E0E0 + #FFFFFF + #FFFFFF + #F5F5F5 + #F0F0F0 + #EEEEEE + #E0E0E0 + #003E34 + #FFFFFF + #237A69 + #FFFFFF + #233B35 + #FFFFFF + #59726B + #FFFFFF + #18394E + #FFFFFF + #517187 + #FFFFFF + #740006 + #FFFFFF + #CF2C27 + #FFFFFF + #F5FBF7 + #171D1B + #F5FBF7 + #0C1211 + #DBE5E0 + #2F3835 + #4B5551 + #656F6C + #000000 + #2B3230 + #ECF2EF + #84D6C2 + #237A69 + #FFFFFF + #006051 + #FFFFFF + #59726B + #FFFFFF + #415A53 + #FFFFFF + #517187 + #FFFFFF + #39586E + #FFFFFF + #C1C8C5 + #F5FBF7 + #FFFFFF + #EFF5F1 + #E3EAE6 + #D8DEDB + #CDD3D0 + #00332A + #FFFFFF + #005346 + #FFFFFF + #18302B + #FFFFFF + #364E47 + #FFFFFF + #0B2F43 + #FFFFFF + #2D4D62 + #FFFFFF + #600004 + #FFFFFF + #98000A + #FFFFFF + #F5FBF7 + #171D1B + #F5FBF7 + #000000 + #DBE5E0 + #000000 + #252E2B + #424B48 + #000000 + #2B3230 + #FFFFFF + #84D6C2 + #005346 + #FFFFFF + #003A30 + #FFFFFF + #364E47 + #FFFFFF + #1F3731 + #FFFFFF + #2D4D62 + #FFFFFF + #14364A + #FFFFFF + #B4BAB7 + #F5FBF7 + #FFFFFF + #ECF2EF + #DEE4E0 + #CFD6D2 + #C1C8C5 - #338574 - #65b5a3 - #2a6e60 - #fe4d2e - #c30000 + #338574 + #EEEEEE + + + @android:color/transparent + @android:color/transparent - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19d10943..10da6e04 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,7 +42,6 @@ Report a bug Read the docs Rate the app ⭐ - Donate 💸 Unsubscribe @@ -310,6 +309,9 @@ Use system default Light mode Dark mode + Dynamic colors + Using the dynamic system colors + Using the ntfy theme colors Backup & Restore Back up to file Export config, notifications, and users diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index 4a3ce4cf..00000000 --- a/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..470ea305 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index b6f5f7f5..77de8393 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -22,6 +22,7 @@ DefaultBaseURL ManageUsers DarkMode + DynamicColors Backup Restore BroadcastEnabled diff --git a/app/src/main/res/xml/detail_preferences.xml b/app/src/main/res/xml/detail_preferences.xml index 38cd18b7..4cb105b1 100644 --- a/app/src/main/res/xml/detail_preferences.xml +++ b/app/src/main/res/xml/detail_preferences.xml @@ -3,7 +3,7 @@ - @@ -35,7 +35,7 @@ app:entryValues="@array/detail_settings_notifications_insistent_max_priority_values" app:defaultValue="-1" app:isPreferenceVisible="false"/> - diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml index 9e766b6d..82d4c9c6 100644 --- a/app/src/main/res/xml/main_preferences.xml +++ b/app/src/main/res/xml/main_preferences.xml @@ -25,7 +25,7 @@ app:entries="@array/settings_notifications_auto_delete_entries" app:entryValues="@array/settings_notifications_auto_delete_values" app:defaultValue="2592000"/> - @@ -37,9 +37,7 @@ + app:title="@string/settings_general_default_base_url_title" /> + - - - diff --git a/fastlane/metadata/android/en-US/changelog/46.txt b/fastlane/metadata/android/en-US/changelog/46.txt new file mode 100644 index 00000000..f94b7f8e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelog/46.txt @@ -0,0 +1,9 @@ +This release makes changes to comply with the Google Play policies. +See https://github.com/binwiederhier/ntfy/issues/1463 for details. + +Changes: +- Remove the "Donate" button from menu (all variants) +- Change default display name from "ntfy.sh/mytopic" to "mytopic" (all variants) +- Remove links to ntfy docs and issue tracker (Play variant only) +- Remove how-to links to ntfy.sh in a few places (Play variant only) +- Remove "Copy topic address" from subscription menu (Play variant only) diff --git a/fastlane/metadata/android/en-US/changelog/NEXT.txt b/fastlane/metadata/android/en-US/changelog/47.txt similarity index 80% rename from fastlane/metadata/android/en-US/changelog/NEXT.txt rename to fastlane/metadata/android/en-US/changelog/47.txt index 412b4045..1b36f54b 100644 --- a/fastlane/metadata/android/en-US/changelog/NEXT.txt +++ b/fastlane/metadata/android/en-US/changelog/47.txt @@ -1,6 +1,7 @@ Features: * Added GIF support for preview images (ntfy-android#76, thanks to @MichaelArkh) * Added WebP support for preview images (ntfy-android#81, thanks to @jokakilla) +* Added UnifiedPush distributor selection support (ntfy-android#137, thanks to @p1gp1g) Bug fixes + maintenance: * Remove REQUEST_INSTALL_PACKAGES permission (#684) diff --git a/fastlane/metadata/android/en-US/changelog/48.txt b/fastlane/metadata/android/en-US/changelog/48.txt new file mode 100644 index 00000000..334e54da --- /dev/null +++ b/fastlane/metadata/android/en-US/changelog/48.txt @@ -0,0 +1,2 @@ +Features: +* Moved the user interface to Material 3 and added dynamic color support (#580, ntfy-android#56, ntfy-android#126, ntfy-android#135, thanks to @Bnyro and @cyb3rko for the implementation, and to @RokeJulianLockhart for reporting) diff --git a/fastlane/metadata/android/mk-MK/title.txt b/fastlane/metadata/android/mk-MK/title.txt new file mode 100644 index 00000000..70d5de05 --- /dev/null +++ b/fastlane/metadata/android/mk-MK/title.txt @@ -0,0 +1 @@ +ntfy - PUT/POST на ваш телефон diff --git a/gradle.properties b/gradle.properties index 3b8190da..a997da7f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,3 +14,4 @@ org.gradle.jvmargs=-Xmx1536m android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official +android.nonTransitiveRClass=false