diff --git a/app/src/main/java/io/heckel/ntfy/db/CustomHeader.kt b/app/src/main/java/io/heckel/ntfy/db/CustomHeader.kt new file mode 100644 index 00000000..6a381f05 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/db/CustomHeader.kt @@ -0,0 +1,10 @@ +package io.heckel.ntfy.db + +/** + * Represents a custom HTTP header for a specific server + */ +data class CustomHeader( + val baseUrl: String, + val name: String, + val value: String +) diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index 4601798f..25139797 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -483,22 +483,71 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas } } - fun getCustomHeaders(): Map { + /** + * Get all custom headers as a list of CustomHeader objects + */ + fun getCustomHeaders(): List { val json = sharedPrefs.getString(SHARED_PREFS_CUSTOM_HEADERS, null) return if (json != null) { try { - val type = object : TypeToken>() {}.type - Gson().fromJson(json, type) ?: emptyMap() + val type = object : TypeToken>() {}.type + Gson().fromJson(json, type) ?: emptyList() } catch (e: Exception) { Log.w("Repository", "Failed to parse custom headers", e) - emptyMap() + emptyList() } } else { - emptyMap() + emptyList() } } - fun setCustomHeaders(headers: Map) { + /** + * Get custom headers for a specific server URL + */ + fun getCustomHeadersForServer(baseUrl: String): List { + return getCustomHeaders().filter { it.baseUrl == baseUrl } + } + + /** + * Add a new custom header + */ + fun addCustomHeader(header: CustomHeader) { + val currentHeaders = getCustomHeaders().toMutableList() + currentHeaders.add(header) + saveCustomHeaders(currentHeaders) + } + + /** + * Update an existing custom header + */ + fun updateCustomHeader(oldHeader: CustomHeader, newHeader: CustomHeader) { + val currentHeaders = getCustomHeaders().toMutableList() + val index = currentHeaders.indexOfFirst { + it.baseUrl == oldHeader.baseUrl && + it.name == oldHeader.name + } + if (index >= 0) { + currentHeaders[index] = newHeader + saveCustomHeaders(currentHeaders) + } + } + + /** + * Delete a custom header + */ + fun deleteCustomHeader(header: CustomHeader) { + val currentHeaders = getCustomHeaders().toMutableList() + currentHeaders.removeAll { + it.baseUrl == header.baseUrl && + it.name == header.name + } + saveCustomHeaders(currentHeaders) + } + + /** + * Save the list of custom headers to SharedPreferences + */ + private fun saveCustomHeaders(headers: List) { val json = if (headers.isEmpty()) { null } else { diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt index 6723e418..5a3fdf15 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -173,22 +173,33 @@ class ApiService(private val context: Context) { } /** - * Interceptor that adds custom headers to all HTTP requests + * Interceptor that adds custom headers to HTTP requests based on the target server */ class CustomHeadersInterceptor(private val repository: Repository) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() - val customHeaders = repository.getCustomHeaders() + val requestUrl = originalRequest.url.toString() - // If no custom headers, proceed with original request + // Extract base URL from the request (protocol + host + port) + val baseUrl = "${originalRequest.url.scheme}://${originalRequest.url.host}" + + if (originalRequest.url.port != 80 && originalRequest.url.port != 443) { + ":${originalRequest.url.port}" + } else { + "" + } + + // Get custom headers for this specific server + val customHeaders = repository.getCustomHeadersForServer(baseUrl) + + // If no custom headers for this server, proceed with original request if (customHeaders.isEmpty()) { return chain.proceed(originalRequest) } // Add custom headers to the request val requestBuilder = originalRequest.newBuilder() - customHeaders.forEach { (name, value) -> - requestBuilder.addHeader(name, value) + customHeaders.forEach { header -> + requestBuilder.addHeader(header.name, header.value) } return chain.proceed(requestBuilder.build()) diff --git a/app/src/main/java/io/heckel/ntfy/msg/CustomHeadersFragment.kt b/app/src/main/java/io/heckel/ntfy/msg/CustomHeadersFragment.kt deleted file mode 100644 index 3582d241..00000000 --- a/app/src/main/java/io/heckel/ntfy/msg/CustomHeadersFragment.kt +++ /dev/null @@ -1,198 +0,0 @@ -package io.heckel.ntfy.ui - -import android.os.Bundle -import android.view.LayoutInflater -import android.widget.EditText -import androidx.appcompat.app.AlertDialog -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import com.google.android.material.textfield.TextInputEditText -import io.heckel.ntfy.R -import io.heckel.ntfy.db.Repository -import io.heckel.ntfy.util.Log - -class CustomHeadersFragment : PreferenceFragmentCompat() { - private lateinit var repository: Repository - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.custom_headers_preferences, rootKey) - repository = Repository.getInstance(requireContext()) - - setupAddHeaderPreference() - loadCustomHeaders() - } - - private fun setupAddHeaderPreference() { - findPreference("add_custom_header")?.setOnPreferenceClickListener { - showAddHeaderDialog() - true - } - } - - private fun loadCustomHeaders() { - val customHeaders = repository.getCustomHeaders() - val preferenceScreen = preferenceScreen - - // Remove existing header preferences (keep only the "Add Header" preference) - val preferencesToRemove = mutableListOf() - for (i in 0 until preferenceScreen.preferenceCount) { - val preference = preferenceScreen.getPreference(i) - if (preference.key != "add_custom_header") { - preferencesToRemove.add(preference) - } - } - preferencesToRemove.forEach { preferenceScreen.removePreference(it) } - - // Add preferences for each custom header - customHeaders.forEach { (name, value) -> - val headerPreference = Preference(requireContext()).apply { - key = "header_$name" - title = name - summary = redactHeaderValue(value) // Show redacted value - setOnPreferenceClickListener { - showEditHeaderDialog(name, value) - true - } - } - preferenceScreen.addPreference(headerPreference) - } - } - - private fun showAddHeaderDialog() { - val dialogView = LayoutInflater.from(requireContext()) - .inflate(R.layout.dialog_custom_header, null) - - val headerNameEdit = dialogView.findViewById(R.id.header_name) - val headerValueEdit = dialogView.findViewById(R.id.header_value) - - AlertDialog.Builder(requireContext()) - .setTitle(R.string.custom_headers_add_title) - .setView(dialogView) - .setPositiveButton(R.string.custom_headers_add) { _, _ -> - val name = headerNameEdit.text.toString().trim() - val value = headerValueEdit.text.toString().trim() - - if (validateHeaderName(name)) { - addCustomHeader(name, value) - } else { - showInvalidHeaderDialog() - } - } - .setNegativeButton(R.string.user_dialog_button_cancel, null) - .show() - } - - private fun showEditHeaderDialog(originalName: String, originalValue: String) { - val dialogView = LayoutInflater.from(requireContext()) - .inflate(R.layout.dialog_custom_header, null) - - val headerNameEdit = dialogView.findViewById(R.id.header_name) - val headerValueEdit = dialogView.findViewById(R.id.header_value) - - headerNameEdit.setText(originalName) - headerValueEdit.setText(originalValue) - - AlertDialog.Builder(requireContext()) - .setTitle(R.string.custom_headers_edit_title) - .setView(dialogView) - .setPositiveButton(R.string.custom_headers_save) { _, _ -> - val name = headerNameEdit.text.toString().trim() - val value = headerValueEdit.text.toString().trim() - - if (validateHeaderName(name)) { - updateCustomHeader(originalName, name, value) - } else { - showInvalidHeaderDialog() - } - } - .setNeutralButton(R.string.custom_headers_delete) { _, _ -> - showDeleteHeaderDialog(originalName) - } - .setNegativeButton(R.string.user_dialog_button_cancel, null) - .show() - } - - private fun showDeleteHeaderDialog(headerName: String) { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.custom_headers_delete_title) - .setMessage(getString(R.string.custom_headers_delete_message, headerName)) - .setPositiveButton(R.string.custom_headers_delete) { _, _ -> - deleteCustomHeader(headerName) - } - .setNegativeButton(R.string.user_dialog_button_cancel, null) - .show() - } - - private fun showInvalidHeaderDialog() { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.custom_headers_error_title) - .setMessage(R.string.custom_headers_invalid_name) - .setPositiveButton(android.R.string.ok, null) - .show() - } - - private fun validateHeaderName(name: String): Boolean { - if (name.isEmpty()) return false - - // HTTP header names should only contain ASCII letters, digits, and hyphens - // and must not start or end with hyphens - val regex = Regex("^[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]$|^[A-Za-z0-9]$") - return regex.matches(name) - } - - private fun addCustomHeader(name: String, value: String) { - try { - val currentHeaders = repository.getCustomHeaders().toMutableMap() - currentHeaders[name] = value - repository.setCustomHeaders(currentHeaders) - loadCustomHeaders() // Refresh the UI - Log.d(TAG, "Added custom header: $name") - } catch (e: Exception) { - Log.w(TAG, "Failed to add custom header", e) - } - } - - private fun updateCustomHeader(originalName: String, newName: String, newValue: String) { - try { - val currentHeaders = repository.getCustomHeaders().toMutableMap() - - // Remove the old header if name changed - if (originalName != newName) { - currentHeaders.remove(originalName) - } - - // Add/update the header - currentHeaders[newName] = newValue - - repository.setCustomHeaders(currentHeaders) - loadCustomHeaders() // Refresh the UI - Log.d(TAG, "Updated custom header: $originalName -> $newName") - } catch (e: Exception) { - Log.w(TAG, "Failed to update custom header", e) - } - } - - private fun deleteCustomHeader(name: String) { - try { - val currentHeaders = repository.getCustomHeaders().toMutableMap() - currentHeaders.remove(name) - repository.setCustomHeaders(currentHeaders) - loadCustomHeaders() // Refresh the UI - Log.d(TAG, "Deleted custom header: $name") - } catch (e: Exception) { - Log.w(TAG, "Failed to delete custom header", e) - } - } - - private fun redactHeaderValue(value: String): String { - return when { - value.isEmpty() -> "(empty)" - value.length <= 3 -> "•".repeat(value.length) - else -> "•".repeat(8) // Always show 8 dots for longer values - } - } - - companion object { - private const val TAG = "CustomHeadersFragment" - } -} \ No newline at end of file diff --git a/app/src/main/java/io/heckel/ntfy/ui/CustomHeaderFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/CustomHeaderFragment.kt new file mode 100644 index 00000000..f734a93c --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/ui/CustomHeaderFragment.kt @@ -0,0 +1,199 @@ +package io.heckel.ntfy.ui + +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import android.widget.Button +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import com.google.android.material.textfield.TextInputEditText +import io.heckel.ntfy.R +import io.heckel.ntfy.db.CustomHeader +import io.heckel.ntfy.util.AfterChangedTextWatcher +import io.heckel.ntfy.util.dangerButton +import io.heckel.ntfy.util.validUrl + +class CustomHeaderFragment : DialogFragment() { + private var header: CustomHeader? = null + private lateinit var baseUrlsInUse: ArrayList + private lateinit var listener: CustomHeaderDialogListener + + private lateinit var baseUrlView: TextInputEditText + private lateinit var headerNameView: TextInputEditText + private lateinit var headerValueView: TextInputEditText + private lateinit var positiveButton: Button + + interface CustomHeaderDialogListener { + fun onAddCustomHeader(dialog: DialogFragment, header: CustomHeader) + fun onUpdateCustomHeader(dialog: DialogFragment, oldHeader: CustomHeader, newHeader: CustomHeader) + fun onDeleteCustomHeader(dialog: DialogFragment, header: CustomHeader) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + listener = activity as CustomHeaderDialogListener + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + // Reconstruct header (if it is present in the bundle) + val baseUrl = arguments?.getString(BUNDLE_BASE_URL) + val headerName = arguments?.getString(BUNDLE_HEADER_NAME) + val headerValue = arguments?.getString(BUNDLE_HEADER_VALUE) + + if (baseUrl != null && headerName != null && headerValue != null) { + header = CustomHeader(baseUrl, headerName, headerValue) + } + + // Required for validation + baseUrlsInUse = arguments?.getStringArrayList(BUNDLE_BASE_URLS_IN_USE) ?: arrayListOf() + + // Build root view + val view = requireActivity().layoutInflater.inflate(R.layout.fragment_custom_header_dialog, null) + + val positiveButtonTextResId = if (header == null) R.string.custom_headers_add else R.string.custom_headers_save + val titleView = view.findViewById(R.id.custom_header_dialog_title) as TextView + val descriptionView = view.findViewById(R.id.custom_header_dialog_description) as TextView + + baseUrlView = view.findViewById(R.id.custom_header_dialog_base_url) + headerNameView = view.findViewById(R.id.custom_header_dialog_name) + headerValueView = view.findViewById(R.id.custom_header_dialog_value) + + if (header == null) { + titleView.text = getString(R.string.custom_headers_add_title) + descriptionView.text = getString(R.string.custom_header_dialog_description_add) + baseUrlView.visibility = View.VISIBLE + } else { + titleView.text = getString(R.string.custom_headers_edit_title) + descriptionView.text = getString(R.string.custom_header_dialog_description_edit) + baseUrlView.visibility = View.GONE + baseUrlView.setText(header!!.baseUrl) + headerNameView.setText(header!!.name) + headerValueView.setText(header!!.value) + } + + // Build dialog + val builder = AlertDialog.Builder(activity) + .setView(view) + .setPositiveButton(positiveButtonTextResId) { _, _ -> + saveClicked() + } + .setNegativeButton(R.string.user_dialog_button_cancel) { _, _ -> + // Do nothing + } + if (header != null) { + builder.setNeutralButton(R.string.custom_headers_delete) { _, _ -> + if (this::listener.isInitialized) { + listener.onDeleteCustomHeader(this, header!!) + } + } + } + val dialog = builder.create() + dialog.setOnShowListener { + positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + + // Delete button should be red + if (header != null) { + dialog + .getButton(AlertDialog.BUTTON_NEUTRAL) + .dangerButton(requireContext()) + } + + // Validate input when typing + val textWatcher = AfterChangedTextWatcher { + validateInput() + } + baseUrlView.addTextChangedListener(textWatcher) + headerNameView.addTextChangedListener(textWatcher) + headerValueView.addTextChangedListener(textWatcher) + + // Focus + if (header != null) { + headerNameView.requestFocus() + if (headerNameView.text != null) { + headerNameView.setSelection(headerNameView.text!!.length) + } + } else { + baseUrlView.requestFocus() + } + + // Validate now! + validateInput() + } + + // Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785) + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + + return dialog + } + + private fun saveClicked() { + if (!this::listener.isInitialized) return + val baseUrl = baseUrlView.text?.toString() ?: "" + val headerName = headerNameView.text?.toString()?.trim() ?: "" + val headerValue = headerValueView.text?.toString()?.trim() ?: "" + + if (header == null) { + val newHeader = CustomHeader(baseUrl, headerName, headerValue) + listener.onAddCustomHeader(this, newHeader) + } else { + val newHeader = CustomHeader( + if (baseUrl.isEmpty()) header!!.baseUrl else baseUrl, + headerName, + headerValue + ) + listener.onUpdateCustomHeader(this, header!!, newHeader) + } + } + + private fun validateInput() { + val baseUrl = baseUrlView.text?.toString() ?: "" + val headerName = headerNameView.text?.toString()?.trim() ?: "" + val headerValue = headerValueView.text?.toString()?.trim() ?: "" + + if (header == null) { + // New header: baseUrl, name, and value required + positiveButton.isEnabled = validUrl(baseUrl) + && headerName.isNotEmpty() + && validateHeaderName(headerName) + && headerValue.isNotEmpty() + } else { + // Editing header: name and value required + positiveButton.isEnabled = headerName.isNotEmpty() + && validateHeaderName(headerName) + && headerValue.isNotEmpty() + } + } + + private fun validateHeaderName(name: String): Boolean { + if (name.isEmpty()) return false + + // HTTP header names should only contain ASCII letters, digits, and hyphens + // and must not start or end with hyphens + val regex = Regex("^[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]$|^[A-Za-z0-9]$") + return regex.matches(name) + } + + companion object { + const val TAG = "NtfyCustomHeaderFragment" + private const val BUNDLE_BASE_URL = "baseUrl" + private const val BUNDLE_HEADER_NAME = "headerName" + private const val BUNDLE_HEADER_VALUE = "headerValue" + private const val BUNDLE_BASE_URLS_IN_USE = "baseUrlsInUse" + + fun newInstance(header: CustomHeader?, baseUrlsInUse: List): CustomHeaderFragment { + val fragment = CustomHeaderFragment() + val args = Bundle() + args.putStringArrayList(BUNDLE_BASE_URLS_IN_USE, ArrayList(baseUrlsInUse)) + if (header != null) { + args.putString(BUNDLE_BASE_URL, header.baseUrl) + args.putString(BUNDLE_HEADER_NAME, header.name) + args.putString(BUNDLE_HEADER_VALUE, header.value) + } + fragment.arguments = args + return fragment + } + } +} 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..56c44434 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -49,9 +49,10 @@ import java.util.concurrent.TimeUnit * https://github.com/googlearchive/android-preferences/blob/master/app/src/main/java/com/example/androidx/preference/sample/MainActivity.kt */ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, - UserFragment.UserDialogListener { + UserFragment.UserDialogListener, CustomHeaderFragment.CustomHeaderDialogListener { private lateinit var settingsFragment: SettingsFragment private lateinit var userSettingsFragment: UserSettingsFragment + private lateinit var customHeaderSettingsFragment: CustomHeaderSettingsFragment private lateinit var repository: Repository private lateinit var serviceManager: SubscriberServiceManager @@ -118,6 +119,10 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere if (fragment is UserSettingsFragment) { userSettingsFragment = fragment } + // Save custom header settings fragment for later + if (fragment is CustomHeaderSettingsFragment) { + customHeaderSettingsFragment = fragment + } return true } @@ -795,6 +800,91 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } } + class CustomHeaderSettingsFragment : PreferenceFragmentCompat() { + private lateinit var repository: Repository + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.custom_header_preferences, rootKey) + repository = Repository.getInstance(requireActivity()) + reload() + } + + data class CustomHeaderWithMetadata( + val baseUrl: String, + val headers: List + ) + + fun reload() { + preferenceScreen.removeAll() + lifecycleScope.launch(Dispatchers.IO) { + val headersByBaseUrl = repository.getCustomHeaders() + .groupBy { it.baseUrl } + .map { entry -> + CustomHeaderWithMetadata(entry.key, entry.value) + } + .sortedBy { it.baseUrl } + + activity?.runOnUiThread { + addCustomHeaderPreferences(headersByBaseUrl) + } + } + } + + private fun addCustomHeaderPreferences(headersByBaseUrl: List) { + val baseUrlsInUse = headersByBaseUrl.map { it.baseUrl } + + headersByBaseUrl.forEach { serverHeaders -> + val baseUrl = serverHeaders.baseUrl + val headers = serverHeaders.headers + + val preferenceCategory = PreferenceCategory(preferenceScreen.context) + preferenceCategory.title = shortUrl(baseUrl) + preferenceScreen.addPreference(preferenceCategory) + + headers.forEach { header -> + val preference = Preference(preferenceScreen.context) + preference.title = header.name + preference.summary = redactHeaderValue(header.value) + preference.onPreferenceClickListener = OnPreferenceClickListener { _ -> + activity?.let { + CustomHeaderFragment + .newInstance(header, baseUrlsInUse) + .show(it.supportFragmentManager, CustomHeaderFragment.TAG) + } + true + } + preferenceCategory.addPreference(preference) + } + } + + // Add header + val headerAddCategory = PreferenceCategory(preferenceScreen.context) + headerAddCategory.title = getString(R.string.settings_general_custom_headers_prefs_header_add) + preferenceScreen.addPreference(headerAddCategory) + + val headerAddPref = Preference(preferenceScreen.context) + headerAddPref.title = getString(R.string.settings_general_custom_headers_prefs_header_add_title) + headerAddPref.summary = getString(R.string.settings_general_custom_headers_prefs_header_add_summary) + headerAddPref.onPreferenceClickListener = OnPreferenceClickListener { _ -> + activity?.let { + CustomHeaderFragment + .newInstance(header = null, baseUrlsInUse = baseUrlsInUse) + .show(it.supportFragmentManager, CustomHeaderFragment.TAG) + } + true + } + headerAddCategory.addPreference(headerAddPref) + } + + private fun redactHeaderValue(value: String): String { + return when { + value.isEmpty() -> "(empty)" + value.length <= 3 -> "•".repeat(value.length) + else -> "•".repeat(8) + } + } + } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD) { @@ -833,6 +923,36 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } } + override fun onAddCustomHeader(dialog: DialogFragment, header: io.heckel.ntfy.db.CustomHeader) { + lifecycleScope.launch(Dispatchers.IO) { + repository.addCustomHeader(header) + serviceManager.restart() // Restart to apply new headers + runOnUiThread { + customHeaderSettingsFragment.reload() + } + } + } + + override fun onUpdateCustomHeader(dialog: DialogFragment, oldHeader: io.heckel.ntfy.db.CustomHeader, newHeader: io.heckel.ntfy.db.CustomHeader) { + lifecycleScope.launch(Dispatchers.IO) { + repository.updateCustomHeader(oldHeader, newHeader) + serviceManager.restart() // Restart to apply header changes + runOnUiThread { + customHeaderSettingsFragment.reload() + } + } + } + + override fun onDeleteCustomHeader(dialog: DialogFragment, header: io.heckel.ntfy.db.CustomHeader) { + lifecycleScope.launch(Dispatchers.IO) { + repository.deleteCustomHeader(header) + serviceManager.restart() + runOnUiThread { + customHeaderSettingsFragment.reload() + } + } + } + private fun setAutoDownload() { if (!this::settingsFragment.isInitialized) return settingsFragment.setAutoDownload() diff --git a/app/src/main/res/layout/dialog_custom_header.xml b/app/src/main/res/layout/dialog_custom_header.xml deleted file mode 100644 index 92e06328..00000000 --- a/app/src/main/res/layout/dialog_custom_header.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_custom_header_dialog.xml b/app/src/main/res/layout/fragment_custom_header_dialog.xml new file mode 100644 index 00000000..74acf3f9 --- /dev/null +++ b/app/src/main/res/layout/fragment_custom_header_dialog.xml @@ -0,0 +1,55 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 177194c1..2dd56300 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -350,15 +350,23 @@ Genaue Alarme ntfy kann genaue Alarme planen. Um WebSockets im Hintergrund wieder zu verbinden, sind genaue Alarme erforderlich. Hier tippen, um die Berechtigung zu widerrufen. ntfy kann keine genauen Alarme planen. Um WebSockets im Hintergrund wieder zu verbinden, sind genaue Alarme erforderlich. Tippe hier, um die Berechtigung zu erteilen. - BenutzerdefinierteHeader - Benutzerdefinierte Header - Benutzerdefinierte HTTP-Header zu allen Anfragen hinzufügen + + + BenutzerdefinierteHeader + Benutzerdefinierte Header + Benutzerdefinierte HTTP-Header pro Server hinzufügen + Benutzerdefinierte Header + Header hinzufügen + Header für einen Server hinzufügen + Header werden mit jeder HTTP-Anfrage an diesen Server gesendet + + Header hinzufügen Neuen benutzerdefinierten HTTP-Header hinzufügen Neuen Header hinzufügen Header bearbeiten Header löschen - Sind Sie sicher, dass Sie den Header „%1$s“ löschen möchten? + Sind Sie sicher, dass Sie den Header „%1$s" löschen möchten? Hinzufügen Speichern Löschen @@ -366,4 +374,7 @@ Ungültige Zeichen im Header-Namen Header-Name (z. B. CF-Access-Client-Id) Header-Wert + Fügen Sie einen benutzerdefinierten HTTP-Header hinzu, der mit jeder Anfrage an den angegebenen Server gesendet wird. + Sie können den Header-Namen/Wert für den ausgewählten Header bearbeiten oder ihn löschen. + Dienst-URL diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19d10943..757cd693 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -356,9 +356,6 @@ Exact alarms ntfy can schedule exact alarms. Exact alarms are required to reconnect WebSockets in the background. Click to revoke the permission. ntfy cannot schedule exact alarms. Exact alarms are required to reconnect WebSockets in the background. Click to grant the permission. - CustomHeaders - Custom Headers - Add custom HTTP headers to all requests About Version ntfy %1$s (%2$s) @@ -404,6 +401,15 @@ Delete user Save + + CustomHeaders + Custom headers + Add custom HTTP headers per server + Custom Headers + Add header + Add a header for a server + Headers are sent with every HTTP request to that server + Add Header Add a new custom HTTP header @@ -418,4 +424,7 @@ Header name contains invalid characters Header name (e.g., CF-Access-Client-Id) Header value + Add a custom HTTP header that will be sent with every request to the specified server. + You may edit the header name/value for the selected header, or delete it. + Service URL diff --git a/app/src/main/res/xml/custom_header_preferences.xml b/app/src/main/res/xml/custom_header_preferences.xml new file mode 100644 index 00000000..34f4e1f7 --- /dev/null +++ b/app/src/main/res/xml/custom_header_preferences.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/xml/custom_headers_preferences.xml b/app/src/main/res/xml/custom_headers_preferences.xml deleted file mode 100644 index 08f981ec..00000000 --- a/app/src/main/res/xml/custom_headers_preferences.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml index 9e766b6d..6e5bd440 100644 --- a/app/src/main/res/xml/main_preferences.xml +++ b/app/src/main/res/xml/main_preferences.xml @@ -45,6 +45,11 @@ app:title="@string/settings_general_users_title" app:summary="@string/settings_general_users_summary" app:fragment="io.heckel.ntfy.ui.SettingsActivity$UserSettingsFragment"/> + -