mirror of
https://github.com/binwiederhier/ntfy-android.git
synced 2024-05-17 11:02:36 +12:00
Merge add8f4b6fe
into c15efff72c
This commit is contained in:
commit
cbc27d2ca6
|
@ -128,4 +128,11 @@ dependencies {
|
|||
|
||||
// Image viewer
|
||||
implementation 'com.github.stfalcon-studio:StfalconImageViewer:v1.0.1'
|
||||
|
||||
// QR Scanner
|
||||
implementation 'com.google.mlkit:barcode-scanning:17.0.0'
|
||||
implementation "androidx.camera:camera-camera2:1.0.1"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.1"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha28"
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- Only required on SDK <= 28 -->
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- To reschedule the websocket retry -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- As of Android 13, we need to ask for permission to post notifications -->
|
||||
<uses-permission android:name="android.permission.CAMERA" /> <!-- For QR Code scanning -->
|
||||
|
||||
<!-- Features -->
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
|
||||
<!--
|
||||
Permission REQUEST_INSTALL_PACKAGES (F-Droid only!):
|
||||
|
@ -163,6 +168,9 @@
|
|||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_notification"/>
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.vision.DEPENDENCIES"
|
||||
android:value="barcode" />
|
||||
|
||||
<!-- FileProvider required for older Android versions (<= P), to allow passing the file URI in the open intent.
|
||||
Avoids "exposed beyond app through Intent.getData" exception, see see https://stackoverflow.com/a/57288352/1440785 -->
|
||||
|
|
|
@ -1,16 +1,34 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.util.SparseArray
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.BuildConfig
|
||||
|
@ -21,6 +39,11 @@ import io.heckel.ntfy.msg.ApiService
|
|||
import io.heckel.ntfy.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
class AddFragment : DialogFragment() {
|
||||
private val api = ApiService()
|
||||
|
@ -288,7 +311,7 @@ class AddFragment : DialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun validateInputSubscribeView() {
|
||||
private fun validateInputSubscribeView(onCompletion: () -> Unit = {}) {
|
||||
if (!this::positiveButton.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".
|
||||
|
@ -333,6 +356,8 @@ class AddFragment : DialogFragment() {
|
|||
} else {
|
||||
positiveButton.isEnabled = validTopic(topic)
|
||||
}
|
||||
|
||||
onCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
510
app/src/main/java/io/heckel/ntfy/ui/AddQrFragment.kt
Normal file
510
app/src/main/java/io/heckel/ntfy/ui/AddQrFragment.kt
Normal file
|
@ -0,0 +1,510 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.util.SparseArray
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.BuildConfig
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.db.User
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import io.heckel.ntfy.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
class AddQrFragment : DialogFragment() {
|
||||
private val api = ApiService()
|
||||
|
||||
private lateinit var repository: Repository
|
||||
private lateinit var subscribeListener: SubscribeListener
|
||||
private lateinit var appBaseUrl: String
|
||||
private var defaultBaseUrl: String? = null
|
||||
|
||||
private lateinit var cameraExecutor: ExecutorService
|
||||
private lateinit var subscribeCameraPreview: PreviewView
|
||||
|
||||
private lateinit var subscribeView: View
|
||||
private lateinit var loginView: View
|
||||
private lateinit var positiveButton: Button
|
||||
private lateinit var negativeButton: Button
|
||||
|
||||
// Subscribe page
|
||||
private lateinit var subscribeInstantDeliveryBox: View
|
||||
private lateinit var subscribeInstantDeliveryCheckbox: CheckBox
|
||||
private lateinit var subscribeInstantDeliveryDescription: View
|
||||
private lateinit var subscribeForegroundDescription: TextView
|
||||
private lateinit var subscribeProgress: ProgressBar
|
||||
private lateinit var subscribeErrorText: TextView
|
||||
private lateinit var subscribeErrorTextImage: View
|
||||
private lateinit var subscribeCameraPreviewPermissionText: TextView
|
||||
|
||||
// Login page
|
||||
private lateinit var loginUsernameText: TextInputEditText
|
||||
private lateinit var loginPasswordText: TextInputEditText
|
||||
private lateinit var loginProgress: ProgressBar
|
||||
private lateinit var loginErrorText: TextView
|
||||
private lateinit var loginErrorTextImage: View
|
||||
|
||||
interface SubscribeListener {
|
||||
fun onSubscribe(topic: String, baseUrl: String, instant: Boolean)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
subscribeListener = activity as SubscribeListener
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
if (activity == null) {
|
||||
throw IllegalStateException("Activity cannot be null")
|
||||
}
|
||||
|
||||
// Dependencies (Fragments need a default constructor)
|
||||
repository = Repository.getInstance(requireActivity())
|
||||
appBaseUrl = getString(R.string.app_base_url)
|
||||
defaultBaseUrl = repository.getDefaultBaseUrl()
|
||||
|
||||
// Build root view
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_add_qr_dialog, null)
|
||||
|
||||
// Main "pages"
|
||||
subscribeView = view.findViewById(R.id.add_dialog_subscribe_view)
|
||||
subscribeView.visibility = View.VISIBLE
|
||||
loginView = view.findViewById(R.id.add_dialog_login_view)
|
||||
loginView.visibility = View.GONE
|
||||
|
||||
// Fields for "subscribe page"
|
||||
subscribeInstantDeliveryBox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_box)
|
||||
subscribeInstantDeliveryCheckbox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_checkbox)
|
||||
subscribeInstantDeliveryDescription = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_description)
|
||||
subscribeForegroundDescription = view.findViewById(R.id.add_dialog_subscribe_foreground_description)
|
||||
subscribeProgress = view.findViewById(R.id.add_dialog_subscribe_progress)
|
||||
subscribeErrorText = view.findViewById(R.id.add_dialog_subscribe_error_text)
|
||||
subscribeErrorText.visibility = View.GONE
|
||||
subscribeErrorTextImage = view.findViewById(R.id.add_dialog_subscribe_error_text_image)
|
||||
subscribeErrorTextImage.visibility = View.GONE
|
||||
subscribeCameraPreview = view.findViewById(R.id.add_dialog_subscribe_camera_preview)
|
||||
subscribeCameraPreviewPermissionText = view.findViewById(R.id.add_dialog_subscribe_camera_preview_denied_text)
|
||||
|
||||
// Fields for "login page"
|
||||
loginUsernameText = view.findViewById(R.id.add_dialog_login_username)
|
||||
loginPasswordText = view.findViewById(R.id.add_dialog_login_password)
|
||||
loginProgress = view.findViewById(R.id.add_dialog_login_progress)
|
||||
loginErrorText = view.findViewById(R.id.add_dialog_login_error_text)
|
||||
loginErrorTextImage = view.findViewById(R.id.add_dialog_login_error_text_image)
|
||||
|
||||
// Set foreground description text
|
||||
subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl))
|
||||
|
||||
// Show/hide based on flavor (faster shortcut for validateInputSubscribeView, which can only run onShow)
|
||||
if (!BuildConfig.FIREBASE_AVAILABLE) {
|
||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Username/password validation on type
|
||||
val loginTextWatcher = AfterChangedTextWatcher {
|
||||
validateInputLoginView()
|
||||
}
|
||||
loginUsernameText.addTextChangedListener(loginTextWatcher)
|
||||
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()
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, _ ->
|
||||
validateInputSubscribeView()
|
||||
}
|
||||
|
||||
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||
checkIfCameraPermissionIsGranted()
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cameraExecutor.shutdown()
|
||||
}
|
||||
|
||||
private fun checkCameraPermissionsRaw(): Boolean {
|
||||
return ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun checkIfCameraPermissionIsGranted() {
|
||||
if (checkCameraPermissionsRaw()) {
|
||||
startCamera()
|
||||
} else {
|
||||
subscribeCameraPreviewPermissionText.visibility = View.VISIBLE
|
||||
val requiredPermissions = arrayOf(Manifest.permission.CAMERA)
|
||||
requestPermissions(requiredPermissions, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (checkCameraPermissionsRaw()) {
|
||||
startCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun vibratePhone(context: Context) {
|
||||
// This is deprecated, but we aren't using a high enough version for the new API
|
||||
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
if (vibrator.hasVibrator()) {
|
||||
// Vibrate for 500 milliseconds
|
||||
vibrator.vibrate(200)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCamera() {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
|
||||
|
||||
subscribeCameraPreviewPermissionText.visibility = View.GONE
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
val preview = Preview.Builder()
|
||||
.build()
|
||||
.also {
|
||||
it.setSurfaceProvider(subscribeCameraPreview.surfaceProvider)
|
||||
}
|
||||
|
||||
val imageAnalyzer = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also {
|
||||
it.setAnalyzer(cameraExecutor, QrCodeAnalyzer { urlString ->
|
||||
var url = URL(urlString)
|
||||
var baseUrl = "${url.protocol}://${url.host}"
|
||||
var route = url.path.replaceFirst("/", "")
|
||||
|
||||
subscribeBaseUrlText.setText(baseUrl)
|
||||
subscribeTopicText.setText(route)
|
||||
validateInputSubscribeView {
|
||||
if (positiveButton.isEnabled) {
|
||||
positiveButtonClick()
|
||||
vibratePhone(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// Select back camera as a default
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
|
||||
try {
|
||||
// Unbind use cases before rebinding
|
||||
cameraProvider.unbindAll()
|
||||
|
||||
// Bind use cases to camera
|
||||
cameraProvider.bindToLifecycle(
|
||||
this, cameraSelector, preview, imageAnalyzer
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(requireContext()))
|
||||
}
|
||||
|
||||
private fun positiveButtonClick() {
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val baseUrl = getBaseUrl()
|
||||
if (subscribeView.visibility == View.VISIBLE) {
|
||||
checkReadAndMaybeShowLogin(baseUrl, topic)
|
||||
} else if (loginView.visibility == View.VISIBLE) {
|
||||
loginAndMaybeDismiss(baseUrl, topic)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkReadAndMaybeShowLogin(baseUrl: String, topic: String) {
|
||||
subscribeProgress.visibility = View.VISIBLE
|
||||
subscribeErrorText.visibility = View.GONE
|
||||
subscribeErrorTextImage.visibility = View.GONE
|
||||
enableSubscribeView(false)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val user = repository.getUser(baseUrl) // May be null
|
||||
val authorized = api.checkAuth(baseUrl, topic, user)
|
||||
if (authorized) {
|
||||
Log.d(TAG, "Access granted to topic ${topicUrl(baseUrl, topic)}")
|
||||
dismissDialog()
|
||||
} else {
|
||||
if (user != null) {
|
||||
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, but user already exists")
|
||||
showErrorAndReenableSubscribeView(getString(R.string.add_dialog_login_error_not_authorized, user.username))
|
||||
} else {
|
||||
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
|
||||
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
||||
activity.runOnUiThread {
|
||||
showLoginView(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Connection to topic failed: ${e.message}", e)
|
||||
showErrorAndReenableSubscribeView(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorAndReenableSubscribeView(message: String?) {
|
||||
val activity = activity ?: return // We may have pressed "Cancel"
|
||||
activity.runOnUiThread {
|
||||
subscribeProgress.visibility = View.GONE
|
||||
subscribeErrorText.visibility = View.VISIBLE
|
||||
subscribeErrorText.text = message
|
||||
subscribeErrorTextImage.visibility = View.VISIBLE
|
||||
enableSubscribeView(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loginAndMaybeDismiss(baseUrl: String, topic: String) {
|
||||
loginProgress.visibility = View.VISIBLE
|
||||
loginErrorText.visibility = View.GONE
|
||||
loginErrorTextImage.visibility = View.GONE
|
||||
enableLoginView(false)
|
||||
val user = User(
|
||||
baseUrl = baseUrl,
|
||||
username = loginUsernameText.text.toString(),
|
||||
password = loginPasswordText.text.toString()
|
||||
)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
Log.d(TAG, "Checking read access for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
||||
try {
|
||||
val authorized = api.checkAuth(baseUrl, topic, user)
|
||||
if (authorized) {
|
||||
Log.d(TAG, "Access granted for user ${user.username} to topic ${topicUrl(baseUrl, topic)}, adding to database")
|
||||
repository.addUser(user)
|
||||
dismissDialog()
|
||||
} else {
|
||||
Log.w(TAG, "Access not allowed for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
||||
showErrorAndReenableLoginView(getString(R.string.add_dialog_login_error_not_authorized, user.username))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Connection to topic failed during login: ${e.message}", e)
|
||||
showErrorAndReenableLoginView(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorAndReenableLoginView(message: String?) {
|
||||
val activity = activity ?: return // We may have pressed "Cancel"
|
||||
activity.runOnUiThread {
|
||||
loginProgress.visibility = View.GONE
|
||||
loginErrorText.visibility = View.VISIBLE
|
||||
loginErrorText.text = message
|
||||
loginErrorTextImage.visibility = View.VISIBLE
|
||||
enableLoginView(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun negativeButtonClick() {
|
||||
if (subscribeView.visibility == View.VISIBLE) {
|
||||
dialog?.cancel()
|
||||
} else if (loginView.visibility == View.VISIBLE) {
|
||||
showSubscribeView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateInputSubscribeView(onCompletion: () -> Unit = {}) {
|
||||
if (!this::positiveButton.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".
|
||||
//TODO: Phil check this
|
||||
val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) {
|
||||
false
|
||||
} else if (defaultBaseUrl == null) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (instantToggleAllowed) {
|
||||
subscribeInstantDeliveryBox.visibility = View.VISIBLE
|
||||
subscribeInstantDeliveryDescription.visibility = if (subscribeInstantDeliveryCheckbox.isChecked) View.VISIBLE else View.GONE
|
||||
subscribeForegroundDescription.visibility = View.GONE
|
||||
} else {
|
||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||
subscribeInstantDeliveryDescription.visibility = View.GONE
|
||||
subscribeForegroundDescription.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
// Enable/disable "Subscribe" button
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val baseUrl = getBaseUrl()
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val subscription = repository.getSubscription(baseUrl, topic)
|
||||
|
||||
activity?.let {
|
||||
it.runOnUiThread {
|
||||
if (subscription != null || DISALLOWED_TOPICS.contains(topic)) {
|
||||
positiveButton.isEnabled = false
|
||||
} else if (subscribeUseAnotherServerCheckbox.isChecked) {
|
||||
positiveButton.isEnabled = validTopic(topic) && validUrl(baseUrl)
|
||||
} else {
|
||||
positiveButton.isEnabled = validTopic(topic)
|
||||
}
|
||||
|
||||
onCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateInputLoginView() {
|
||||
if (!this::positiveButton.isInitialized || !this::loginUsernameText.isInitialized || !this::loginPasswordText.isInitialized) {
|
||||
return // As per crash seen in Google Play
|
||||
}
|
||||
if (loginUsernameText.visibility == View.GONE) {
|
||||
positiveButton.isEnabled = true
|
||||
} else {
|
||||
positiveButton.isEnabled = (loginUsernameText.text?.isNotEmpty() ?: false)
|
||||
&& (loginPasswordText.text?.isNotEmpty() ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissDialog() {
|
||||
Log.d(TAG, "Closing dialog and calling onSubscribe handler")
|
||||
val activity = activity?: return // We may have pressed "Cancel"
|
||||
activity.runOnUiThread {
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val baseUrl = getBaseUrl()
|
||||
val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
||||
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBaseUrl(): String {
|
||||
return if (subscribeUseAnotherServerCheckbox.isChecked) {
|
||||
subscribeBaseUrlText.text.toString()
|
||||
} else {
|
||||
return defaultBaseUrl ?: appBaseUrl
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSubscribeView() {
|
||||
resetSubscribeView()
|
||||
positiveButton.text = getString(R.string.add_dialog_button_subscribe)
|
||||
negativeButton.text = getString(R.string.add_dialog_button_cancel)
|
||||
loginView.visibility = View.GONE
|
||||
subscribeView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
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)
|
||||
subscribeView.visibility = View.GONE
|
||||
loginView.visibility = View.VISIBLE
|
||||
if (loginUsernameText.requestFocus()) {
|
||||
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableSubscribeView(enable: Boolean) {
|
||||
subscribeInstantDeliveryCheckbox.isEnabled = enable
|
||||
positiveButton.isEnabled = enable
|
||||
}
|
||||
|
||||
private fun resetSubscribeView() {
|
||||
subscribeProgress.visibility = View.GONE
|
||||
subscribeErrorText.visibility = View.GONE
|
||||
subscribeErrorTextImage.visibility = View.GONE
|
||||
enableSubscribeView(true)
|
||||
}
|
||||
|
||||
private fun enableLoginView(enable: Boolean) {
|
||||
loginUsernameText.isEnabled = enable
|
||||
loginPasswordText.isEnabled = enable
|
||||
positiveButton.isEnabled = enable
|
||||
if (enable && loginUsernameText.requestFocus()) {
|
||||
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetLoginView() {
|
||||
loginProgress.visibility = View.GONE
|
||||
loginErrorText.visibility = View.GONE
|
||||
loginErrorTextImage.visibility = View.GONE
|
||||
loginUsernameText.visibility = View.VISIBLE
|
||||
loginUsernameText.text?.clear()
|
||||
loginPasswordText.visibility = View.VISIBLE
|
||||
loginPasswordText.text?.clear()
|
||||
enableLoginView(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NtfyAddFragment"
|
||||
private val DISALLOWED_TOPICS = listOf("docs", "static", "file") // If updated, also update in server
|
||||
}
|
||||
}
|
|
@ -65,6 +65,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
private lateinit var mainListContainer: SwipeRefreshLayout
|
||||
private lateinit var adapter: MainAdapter
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var fabAddNormal: FloatingActionButton
|
||||
private lateinit var fabAddNormalLabel: TextView
|
||||
private lateinit var fabAddQr: FloatingActionButton
|
||||
private lateinit var fabAddQrLabel: TextView
|
||||
|
||||
// Other stuff
|
||||
private var actionMode: ActionMode? = null
|
||||
|
@ -87,12 +91,32 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
// Action bar
|
||||
title = getString(R.string.main_action_bar_title)
|
||||
|
||||
// Floating action button ("+")
|
||||
// Floating action button and menu options ("+")
|
||||
var isFabOpen = false
|
||||
fab = findViewById(R.id.fab)
|
||||
fabAddNormal = findViewById(R.id.fab_add_normal)
|
||||
fabAddNormalLabel = findViewById(R.id.fab_add_normal_label)
|
||||
fabAddQr = findViewById(R.id.fab_add_qr)
|
||||
fabAddQrLabel = findViewById(R.id.fab_add_qr_label)
|
||||
|
||||
fab.setOnClickListener {
|
||||
if (!isFabOpen) {
|
||||
showFabMenu()
|
||||
isFabOpen = true
|
||||
} else {
|
||||
closeFabMenu()
|
||||
isFabOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
fabAddNormal.setOnClickListener {
|
||||
onSubscribeButtonClick()
|
||||
}
|
||||
|
||||
fabAddQr.setOnClickListener {
|
||||
onSubscribeQrButtonClick()
|
||||
}
|
||||
|
||||
// Swipe to refresh
|
||||
mainListContainer = findViewById(R.id.main_subscriptions_list_container)
|
||||
mainListContainer.setOnRefreshListener { refreshAllSubscriptions() }
|
||||
|
@ -214,6 +238,54 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
maybeRequestNotificationPermission()
|
||||
}
|
||||
|
||||
private fun showFabMenu() {
|
||||
fabAddNormal.alpha = 0f
|
||||
fabAddNormal.visibility = View.VISIBLE
|
||||
fabAddNormal.animate().alpha(1f).translationY(-45.0f).setDuration(300).start()
|
||||
|
||||
fabAddNormalLabel.visibility = View.VISIBLE
|
||||
fabAddNormalLabel.alpha = 0f
|
||||
fabAddNormalLabel.animate().alpha(1f).translationY(-45.0f).setDuration(300).start()
|
||||
|
||||
fabAddQr.alpha = 0f
|
||||
fabAddQr.visibility = View.VISIBLE
|
||||
fabAddQr.animate().alpha(1f).translationY(-85.0f).setDuration(300).start()
|
||||
|
||||
fabAddQrLabel.visibility = View.VISIBLE
|
||||
fabAddQrLabel.alpha = 0f
|
||||
fabAddQrLabel.animate().alpha(1f).translationY(-85.0f).setDuration(300).start()
|
||||
|
||||
fab.animate().rotation(45.0f).setDuration(300).start()
|
||||
}
|
||||
|
||||
private fun closeFabMenu() {
|
||||
fabAddNormal.animate()
|
||||
.translationY(0f)
|
||||
.alpha(0f)
|
||||
.setDuration(300)
|
||||
.withEndAction { fabAddNormal.visibility = View.GONE }
|
||||
|
||||
fabAddNormalLabel.animate()
|
||||
.translationY(0f)
|
||||
.alpha(0f)
|
||||
.setDuration(300)
|
||||
.withEndAction { fabAddNormalLabel.visibility = View.GONE }
|
||||
|
||||
fabAddQr.animate()
|
||||
.translationY(0f)
|
||||
.alpha(0f)
|
||||
.setDuration(300)
|
||||
.withEndAction { fabAddQr.visibility = View.GONE }
|
||||
|
||||
fabAddQrLabel.animate()
|
||||
.translationY(0f)
|
||||
.alpha(0f)
|
||||
.setDuration(300)
|
||||
.withEndAction { fabAddQrLabel.visibility = View.GONE }
|
||||
|
||||
fab.animate().rotation(0.0f).setDuration(300).start()
|
||||
}
|
||||
|
||||
private fun maybeRequestNotificationPermission() {
|
||||
// Android 13 (SDK 33) requires that we ask for permission to post notifications
|
||||
// https://developer.android.com/develop/ui/views/notifications/notification-permission
|
||||
|
@ -441,6 +513,11 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
newFragment.show(supportFragmentManager, AddFragment.TAG)
|
||||
}
|
||||
|
||||
private fun onSubscribeQrButtonClick() {
|
||||
val newFragment = AddQrFragment()
|
||||
newFragment.show(supportFragmentManager, AddFragment.TAG)
|
||||
}
|
||||
|
||||
override fun onSubscribe(topic: String, baseUrl: String, instant: Boolean) {
|
||||
Log.d(TAG, "Adding subscription ${topicShortUrl(baseUrl, topic)} (instant = $instant)")
|
||||
|
||||
|
|
58
app/src/main/java/io/heckel/ntfy/util/QrCodeAnalyzer.kt
Normal file
58
app/src/main/java/io/heckel/ntfy/util/QrCodeAnalyzer.kt
Normal file
|
@ -0,0 +1,58 @@
|
|||
package io.heckel.ntfy.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import com.google.mlkit.vision.barcode.Barcode
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
|
||||
typealias BarcodeScannedCallback = (barcode: String) -> Unit
|
||||
|
||||
class QrCodeAnalyzer(private val onBarcodeScanned: BarcodeScannedCallback) : ImageAnalysis.Analyzer {
|
||||
|
||||
private var lastSuccessfulScanTimestamp: Long = 0
|
||||
private var lastProcessedTimestamp: Long = 0
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun analyze(image: ImageProxy) {
|
||||
if (System.currentTimeMillis() - lastSuccessfulScanTimestamp < 3000) {
|
||||
image.close()
|
||||
return
|
||||
} else if (System.currentTimeMillis() - lastProcessedTimestamp < 100) {
|
||||
image.close()
|
||||
return
|
||||
}
|
||||
|
||||
val img = image.image
|
||||
if (img != null) {
|
||||
val inputImage = InputImage.fromMediaImage(img, image.imageInfo.rotationDegrees)
|
||||
|
||||
// Process image searching for barcodes
|
||||
val options = BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.build()
|
||||
|
||||
val scanner = BarcodeScanning.getClient(options)
|
||||
|
||||
scanner.process(inputImage)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
for (barcode in barcodes) {
|
||||
if (barcode.valueType !== Barcode.TYPE_URL) continue
|
||||
|
||||
onBarcodeScanned(barcodes[0].url.url)
|
||||
lastSuccessfulScanTimestamp = System.currentTimeMillis()
|
||||
break
|
||||
}
|
||||
}
|
||||
.addOnFailureListener {
|
||||
}
|
||||
.addOnCompleteListener {
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
|
||||
lastProcessedTimestamp = System.currentTimeMillis()
|
||||
}
|
||||
}
|
15
app/src/main/res/drawable/ic_qr_code_24dp.xml
Normal file
15
app/src/main/res/drawable/ic_qr_code_24dp.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,19h2v2h-2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M13,13h2v2h-2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15,15h2v2h-2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M13,17h2v2h-2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15,19h2v2h-2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,17h2v2h-2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,13h2v2h-2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,15h2v2h-2z"/>
|
||||
</vector>
|
|
@ -208,16 +208,71 @@
|
|||
android:autoLink="web"/>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_margin="24dp">
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="24dp"
|
||||
android:contentDescription="@string/main_add_button_description"
|
||||
android:src="@drawable/ic_add_black_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
style="@style/FloatingActionButton"
|
||||
/>
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
style="@style/FloatingActionButton" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_add_normal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/main_add_normal_button_description"
|
||||
android:src="@drawable/ic_add_black_24dp"
|
||||
android:layout_above="@id/fab"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:backgroundTint="@color/gray_400"
|
||||
android:tint="@color/gray_400"
|
||||
app:fabSize="mini"
|
||||
app:elevation="4dp"
|
||||
android:visibility="gone" />
|
||||
<TextView
|
||||
android:id="@+id/fab_add_normal_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_toLeftOf="@id/fab_add_normal"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_alignTop="@id/fab_add_normal"
|
||||
android:layout_alignBottom="@id/fab_add_normal"
|
||||
android:text="@string/main_add_normal_button_description"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_add_qr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/main_add_qr_button_description"
|
||||
android:src="@drawable/ic_qr_code_24dp"
|
||||
android:layout_above="@id/fab_add_normal"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:backgroundTint="@color/gray_400"
|
||||
android:tint="@color/gray_400"
|
||||
app:fabSize="mini"
|
||||
app:elevation="4dp"
|
||||
android:visibility="gone" />
|
||||
<TextView
|
||||
android:id="@+id/fab_add_qr_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_toLeftOf="@id/fab_add_qr"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_alignTop="@id/fab_add_qr"
|
||||
android:layout_alignBottom="@id/fab_add_qr"
|
||||
android:text="@string/main_add_qr_button_description"
|
||||
android:visibility="gone" />
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_dialog_subscribe_view">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_dialog_subscribe_view">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_subscribe_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:text="@string/add_dialog_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/add_dialog_subscribe_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:text="@string/add_dialog_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
|
@ -35,7 +35,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_subscribe_description"
|
||||
android:indeterminate="true" android:layout_marginBottom="5dp" android:visibility="gone"/>
|
||||
<TextView
|
||||
<TextView
|
||||
android:text="@string/add_dialog_description_below"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_description"
|
||||
|
@ -50,7 +50,7 @@
|
|||
android:maxLines="1" android:inputType="text" android:maxLength="64"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_description"/>
|
||||
<CheckBox
|
||||
<CheckBox
|
||||
android:text="@string/add_dialog_use_another_server"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_use_another_server_checkbox"
|
||||
|
@ -59,111 +59,112 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_topic_text"
|
||||
android:layout_marginTop="-3dp"/>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_use_another_server_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_use_another_server_description"
|
||||
android:paddingStart="4dp" android:paddingTop="0dp"
|
||||
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_use_another_server_checkbox"
|
||||
android:layout_marginTop="-5dp"/>
|
||||
android:text="@string/add_dialog_use_another_server_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_use_another_server_description"
|
||||
android:paddingStart="4dp" android:paddingTop="0dp"
|
||||
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_use_another_server_checkbox"
|
||||
android:layout_marginTop="-5dp"/>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu"
|
||||
android:id="@+id/add_dialog_subscribe_base_url_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense.ExposedDropdownMenu"
|
||||
android:id="@+id/add_dialog_subscribe_base_url_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="0dp"
|
||||
android:padding="0dp"
|
||||
android:visibility="gone"
|
||||
app:endIconMode="custom"
|
||||
app:hintEnabled="false"
|
||||
app:boxBackgroundColor="@null"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_use_another_server_description">
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="0dp"
|
||||
android:padding="0dp"
|
||||
android:visibility="gone"
|
||||
app:endIconMode="custom"
|
||||
app:hintEnabled="false"
|
||||
app:boxBackgroundColor="@null"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_use_another_server_description">
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/add_dialog_subscribe_base_url_text"
|
||||
android:hint="@string/app_base_url"
|
||||
android:maxLines="1"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
/>
|
||||
android:id="@+id/add_dialog_subscribe_base_url_text"
|
||||
android:hint="@string/app_base_url"
|
||||
android:maxLines="1"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_box"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_base_url_layout" android:layout_marginTop="-3dp">
|
||||
<CheckBox
|
||||
<CheckBox
|
||||
android:text="@string/add_dialog_instant_delivery"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_checkbox"
|
||||
android:layout_marginTop="-8dp" android:layout_marginBottom="-5dp"
|
||||
android:layout_marginStart="-3dp"/>
|
||||
<ImageView
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_gray_24dp"
|
||||
android:id="@+id/add_dialog_subscribe_instant_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_item_text"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_item_date" android:paddingTop="3dp"
|
||||
android:layout_marginTop="3dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_instant_delivery_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_description"
|
||||
android:paddingStart="4dp" android:paddingTop="0dp"
|
||||
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_box"/>
|
||||
android:text="@string/add_dialog_instant_delivery_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_description"
|
||||
android:paddingStart="4dp" android:paddingTop="0dp"
|
||||
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_box"/>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_foreground_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_foreground_description"
|
||||
android:paddingStart="4dp" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_description"/>
|
||||
android:text="@string/add_dialog_foreground_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_foreground_description"
|
||||
android:paddingStart="4dp" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_description"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||
android:id="@+id/add_dialog_subscribe_error_text_image"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/add_dialog_subscribe_error_text" android:layout_marginTop="1dp"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/add_dialog_subscribe_error_text"
|
||||
android:layout_marginTop="1dp"/>
|
||||
<TextView
|
||||
android:text="Unable to resolve host example.com"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_error_text"
|
||||
android:paddingStart="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_foreground_description"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/DangerText"
|
||||
app:layout_constraintStart_toEndOf="@id/add_dialog_subscribe_error_text_image"
|
||||
android:layout_marginTop="5dp"
|
||||
tools:visibility="gone"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:text="Unable to resolve host example.com"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_error_text"
|
||||
android:paddingStart="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_foreground_description"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/DangerText"
|
||||
app:layout_constraintStart_toEndOf="@id/add_dialog_subscribe_error_text_image"
|
||||
android:layout_marginTop="5dp"
|
||||
tools:visibility="gone"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_dialog_login_view">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_dialog_login_view">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_login_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -174,15 +175,15 @@
|
|||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
<TextView
|
||||
/>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_login_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_description"
|
||||
android:paddingStart="4dp" android:paddingTop="3dp" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_title" android:paddingEnd="4dp"/>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/add_dialog_login_username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_username_hint"
|
||||
|
@ -190,7 +191,7 @@
|
|||
android:maxLines="1" android:inputType="text" android:maxLength="64"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="10dp" app:layout_constraintTop_toBottomOf="@+id/add_dialog_login_description"/>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/add_dialog_login_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_password_hint"
|
||||
|
@ -198,13 +199,13 @@
|
|||
android:maxLines="1" android:inputType="textPassword" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_username"/>
|
||||
<ImageView
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||
android:id="@+id/add_dialog_login_error_text_image"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@+id/add_dialog_login_error_text" app:layout_constraintTop_toTopOf="@+id/add_dialog_login_error_text"/>
|
||||
<TextView
|
||||
<TextView
|
||||
android:text="Login failed. User not authorized."
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_error_text"
|
||||
|
@ -214,7 +215,7 @@
|
|||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/DangerText"
|
||||
app:layout_constraintStart_toEndOf="@id/add_dialog_login_error_text_image"/>
|
||||
<ProgressBar
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
|
@ -222,6 +223,6 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description"
|
||||
android:indeterminate="true" android:layout_marginBottom="5dp"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
|
189
app/src/main/res/layout/fragment_add_qr_dialog.xml
Normal file
189
app/src/main/res/layout/fragment_add_qr_dialog.xml
Normal file
|
@ -0,0 +1,189 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_dialog_subscribe_view">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_subscribe_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:text="@string/add_dialog_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/add_dialog_subscribe_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_subscribe_title"
|
||||
android:indeterminate="true" android:layout_marginBottom="5dp" android:visibility="gone"/>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_box"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_title" android:layout_marginTop="6dp">
|
||||
<CheckBox
|
||||
android:text="@string/add_dialog_instant_delivery"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_checkbox"
|
||||
android:layout_marginTop="-8dp" android:layout_marginBottom="-5dp"
|
||||
android:layout_marginStart="-3dp"/>
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_gray_24dp"
|
||||
android:id="@+id/add_dialog_subscribe_instant_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_item_text"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_item_date" android:paddingTop="3dp"
|
||||
android:layout_marginTop="3dp"/>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_instant_delivery_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_description"
|
||||
android:paddingStart="4dp" android:paddingTop="0dp"
|
||||
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_box"/>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_foreground_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_foreground_description"
|
||||
android:paddingStart="4dp" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_description"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||
android:id="@+id/add_dialog_subscribe_error_text_image"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/add_dialog_subscribe_error_text"
|
||||
android:layout_marginTop="1dp"/>
|
||||
<TextView
|
||||
android:text="Unable to resolve host example.com"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_error_text"
|
||||
android:paddingStart="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_foreground_description"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/DangerText"
|
||||
app:layout_constraintStart_toEndOf="@id/add_dialog_subscribe_error_text_image"
|
||||
android:layout_marginTop="5dp"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_error_text">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Scan ntfy QR code:" />
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/add_dialog_subscribe_camera_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp"
|
||||
android:background="#000" />
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_subscribe_camera_preview_denied_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Camera permission is required to scan QR codes."
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_dialog_login_view">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_login_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:text="@string/add_dialog_login_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_login_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_description"
|
||||
android:paddingStart="4dp" android:paddingTop="3dp" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_title" android:paddingEnd="4dp"/>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/add_dialog_login_username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_username_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLines="1" android:inputType="text" android:maxLength="64"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="10dp" app:layout_constraintTop_toBottomOf="@+id/add_dialog_login_description"/>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/add_dialog_login_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_password_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLines="1" android:inputType="textPassword" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_username"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||
android:id="@+id/add_dialog_login_error_text_image"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@+id/add_dialog_login_error_text" app:layout_constraintTop_toTopOf="@+id/add_dialog_login_error_text"/>
|
||||
<TextView
|
||||
android:text="Login failed. User not authorized."
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_error_text"
|
||||
android:paddingStart="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_password"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/DangerText"
|
||||
app:layout_constraintStart_toEndOf="@id/add_dialog_login_error_text_image"/>
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:id="@+id/add_dialog_login_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description"
|
||||
android:indeterminate="true" android:layout_marginBottom="5dp"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
|
@ -60,6 +60,8 @@
|
|||
<string name="main_item_status_unified_push">%1$s (UnifiedPush)</string>
|
||||
<string name="main_item_date_yesterday">yesterday</string>
|
||||
<string name="main_add_button_description">Add subscription</string>
|
||||
<string name="main_add_normal_button_description">Add subscription</string>
|
||||
<string name="main_add_qr_button_description">Add subscription with QR Code</string>
|
||||
<string name="main_no_subscriptions_text">It looks like you don\'t have any subscriptions yet.</string>
|
||||
<string name="main_how_to_intro">
|
||||
Click the + to create or subscribe to a topic.
|
||||
|
|
Loading…
Reference in a new issue