Server setup improvements (#67)
* Improve AddServer fragment * Improve login fragment With other general improvements * Resize the addserver and login fragments when the soft keyboard appears * Upgrade androidx.core to 1.7.0 and add lifecycle deps * New UI state system for AddServerFragment This uses StateFlow for the state and SharedFlow for navigation * Remove public flows and use collector functions * Update Login ViewModel and Fragment * Speed up server discovery * Better login error message
This commit is contained in:
parent
44d0a34539
commit
98cb038c24
13 changed files with 516 additions and 294 deletions
|
@ -33,7 +33,7 @@
|
|||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FindroidSplashScreen"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
@ -12,7 +12,7 @@ interface ServerDatabaseDao {
|
|||
fun update(server: Server)
|
||||
|
||||
@Query("select * from servers where id = :id")
|
||||
fun get(id: String): Server
|
||||
fun get(id: String): Server?
|
||||
|
||||
@Query("delete from servers")
|
||||
fun clear()
|
||||
|
|
|
@ -26,7 +26,7 @@ object ApiModule {
|
|||
|
||||
val serverId = sharedPreferences.getString("selectedServer", null)
|
||||
if (serverId != null) {
|
||||
val server = serverDatabase.get(serverId)
|
||||
val server = serverDatabase.get(serverId) ?: return jellyfinApi
|
||||
jellyfinApi.apply {
|
||||
api.baseUrl = server.address
|
||||
api.accessToken = server.accessToken
|
||||
|
|
|
@ -4,13 +4,19 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.databinding.FragmentAddServerBinding
|
||||
import dev.jdtech.jellyfin.viewmodels.AddServerViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AddServerFragment : Fragment() {
|
||||
|
@ -27,33 +33,62 @@ class AddServerFragment : Fragment() {
|
|||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.buttonConnect.setOnClickListener {
|
||||
val serverAddress = binding.editTextServerAddress.text.toString()
|
||||
if (serverAddress.isNotBlank()) {
|
||||
viewModel.checkServer(serverAddress, resources)
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
binding.editTextServerAddressLayout.error = ""
|
||||
} else {
|
||||
binding.editTextServerAddressLayout.error = resources.getString(R.string.add_server_error_empty_address)
|
||||
binding.editTextServerAddress.setOnEditorActionListener { _, actionId, _ ->
|
||||
return@setOnEditorActionListener when (actionId) {
|
||||
EditorInfo.IME_ACTION_GO -> {
|
||||
connectToServer()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.navigateToLogin.observe(viewLifecycleOwner, {
|
||||
if (it) {
|
||||
navigateToLoginFragment()
|
||||
}
|
||||
binding.progressCircular.visibility = View.GONE
|
||||
})
|
||||
binding.buttonConnect.setOnClickListener {
|
||||
connectToServer()
|
||||
}
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, {
|
||||
binding.editTextServerAddressLayout.error = it
|
||||
})
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is AddServerViewModel.UiState.Normal -> bindUiStateNormal()
|
||||
is AddServerViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
is AddServerViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
}
|
||||
}
|
||||
viewModel.onNavigateToLogin(viewLifecycleOwner.lifecycleScope) {
|
||||
Timber.d("Navigate to login: $it")
|
||||
if (it) {
|
||||
navigateToLoginFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal() {
|
||||
binding.progressCircular.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: AddServerViewModel.UiState.Error) {
|
||||
binding.progressCircular.isVisible = false
|
||||
binding.editTextServerAddressLayout.error = uiState.message
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.progressCircular.isVisible = true
|
||||
binding.editTextServerAddressLayout.error = null
|
||||
}
|
||||
|
||||
private fun connectToServer() {
|
||||
val serverAddress = binding.editTextServerAddress.text.toString()
|
||||
viewModel.checkServer(serverAddress)
|
||||
}
|
||||
|
||||
private fun navigateToLoginFragment() {
|
||||
findNavController().navigate(AddServerFragmentDirections.actionAddServerFragment3ToLoginFragment2())
|
||||
viewModel.onNavigateToLoginDone()
|
||||
}
|
||||
}
|
|
@ -4,50 +4,93 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.databinding.FragmentLoginBinding
|
||||
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LoginFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentLoginBinding
|
||||
private val viewModel: LoginViewModel by viewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentLoginBinding.inflate(inflater)
|
||||
binding = FragmentLoginBinding.inflate(inflater)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.buttonLogin.setOnClickListener {
|
||||
val username = binding.editTextUsername.text.toString()
|
||||
val password = binding.editTextPassword.text.toString()
|
||||
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
viewModel.login(username, password)
|
||||
binding.editTextPassword.setOnEditorActionListener { _, actionId, _ ->
|
||||
return@setOnEditorActionListener when (actionId) {
|
||||
EditorInfo.IME_ACTION_GO -> {
|
||||
login()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, {
|
||||
binding.progressCircular.visibility = View.GONE
|
||||
binding.editTextUsernameLayout.error = it
|
||||
})
|
||||
binding.buttonLogin.setOnClickListener {
|
||||
login()
|
||||
}
|
||||
|
||||
viewModel.navigateToMain.observe(viewLifecycleOwner, {
|
||||
if (it) {
|
||||
navigateToMainActivity()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when(uiState) {
|
||||
is LoginViewModel.UiState.Normal -> bindUiStateNormal()
|
||||
is LoginViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
is LoginViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
}
|
||||
}
|
||||
viewModel.onNavigateToMain(viewLifecycleOwner.lifecycleScope) {
|
||||
Timber.d("Navigate to MainActivity: $it")
|
||||
if (it) {
|
||||
navigateToMainActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal() {
|
||||
binding.progressCircular.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) {
|
||||
binding.progressCircular.isVisible = false
|
||||
binding.editTextUsernameLayout.error = uiState.message
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.progressCircular.isVisible = true
|
||||
binding.editTextUsernameLayout.error = null
|
||||
}
|
||||
|
||||
private fun login() {
|
||||
val username = binding.editTextUsername.text.toString()
|
||||
val password = binding.editTextPassword.text.toString()
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
viewModel.login(username, password)
|
||||
}
|
||||
|
||||
private fun navigateToMainActivity() {
|
||||
findNavController().navigate(LoginFragmentDirections.actionLoginFragment2ToNavigationHome())
|
||||
viewModel.doneNavigatingToMain()
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ class MediaFragment : Fragment() {
|
|||
private lateinit var binding: FragmentMediaBinding
|
||||
private val viewModel: MediaViewModel by viewModels()
|
||||
|
||||
private var originalSoftInputMode: Int? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
@ -33,7 +35,7 @@ class MediaFragment : Fragment() {
|
|||
val searchView = search.actionView as SearchView
|
||||
searchView.queryHint = getString(R.string.search_hint)
|
||||
|
||||
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener {
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(p0: String?): Boolean {
|
||||
if (p0 != null) {
|
||||
navigateToSearchResultFragment(p0)
|
||||
|
@ -81,12 +83,28 @@ class MediaFragment : Fragment() {
|
|||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
||||
parentFragmentManager,
|
||||
"errordialog"
|
||||
)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
requireActivity().window.let {
|
||||
originalSoftInputMode = it.attributes?.softInputMode
|
||||
it.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
originalSoftInputMode?.let { activity?.window?.setSoftInputMode(it) }
|
||||
}
|
||||
|
||||
private fun navigateToLibraryFragment(library: BaseItemDto) {
|
||||
findNavController().navigate(
|
||||
MediaFragmentDirections.actionNavigationMediaToLibraryFragment(
|
||||
|
|
|
@ -4,16 +4,21 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.databinding.TvAddServerFragmentBinding
|
||||
import dev.jdtech.jellyfin.viewmodels.AddServerViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class TvAddServerFragment: Fragment() {
|
||||
internal class TvAddServerFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: TvAddServerFragmentBinding
|
||||
private val viewModel: AddServerViewModel by viewModels()
|
||||
|
@ -29,30 +34,46 @@ internal class TvAddServerFragment: Fragment() {
|
|||
|
||||
binding.buttonConnect.setOnClickListener {
|
||||
val serverAddress = binding.serverAddress.text.toString()
|
||||
if (serverAddress.isNotBlank()) {
|
||||
viewModel.checkServer(serverAddress, resources)
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.serverAddress.error = resources.getString(R.string.add_server_empty_error)
|
||||
}
|
||||
viewModel.checkServer(serverAddress)
|
||||
}
|
||||
|
||||
viewModel.navigateToLogin.observe(viewLifecycleOwner, {
|
||||
if (it) {
|
||||
navigateToLoginFragment()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is AddServerViewModel.UiState.Normal -> bindUiStateNormal()
|
||||
is AddServerViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
is AddServerViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
}
|
||||
}
|
||||
viewModel.onNavigateToLogin(viewLifecycleOwner.lifecycleScope) {
|
||||
Timber.d("Navigate to login: $it")
|
||||
if (it) {
|
||||
navigateToLoginFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.progressCircular.visibility = View.GONE
|
||||
})
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, {
|
||||
binding.serverAddress.error = it
|
||||
})
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal() {
|
||||
binding.progressCircular.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: AddServerViewModel.UiState.Error) {
|
||||
binding.progressCircular.isVisible = false
|
||||
binding.serverAddress.error = uiState.message
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.progressCircular.isVisible = true
|
||||
binding.serverAddress.error = null
|
||||
}
|
||||
|
||||
private fun navigateToLoginFragment() {
|
||||
findNavController().navigate(TvAddServerFragmentDirections.actionAddServerFragmentToLoginFragment())
|
||||
viewModel.onNavigateToLoginDone()
|
||||
}
|
||||
}
|
|
@ -4,23 +4,30 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.databinding.TvLoginFragmentBinding
|
||||
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TvLoginFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: TvLoginFragmentBinding
|
||||
private val viewModel: LoginViewModel by viewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = TvLoginFragmentBinding.inflate(inflater)
|
||||
binding = TvLoginFragmentBinding.inflate(inflater)
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
|
||||
|
@ -32,22 +39,43 @@ class TvLoginFragment : Fragment() {
|
|||
viewModel.login(username, password)
|
||||
}
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, {
|
||||
binding.progressCircular.visibility = View.GONE
|
||||
binding.username.error = it
|
||||
})
|
||||
|
||||
viewModel.navigateToMain.observe(viewLifecycleOwner, {
|
||||
if (it) {
|
||||
navigateToMainActivity()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when(uiState) {
|
||||
is LoginViewModel.UiState.Normal -> bindUiStateNormal()
|
||||
is LoginViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
is LoginViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
}
|
||||
}
|
||||
viewModel.onNavigateToMain(viewLifecycleOwner.lifecycleScope) {
|
||||
Timber.d("Navigate to MainActivity: $it")
|
||||
if (it) {
|
||||
navigateToMainActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal() {
|
||||
binding.progressCircular.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) {
|
||||
binding.progressCircular.isVisible = false
|
||||
binding.username.error = uiState.message
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.progressCircular.isVisible = true
|
||||
binding.username.error = null
|
||||
}
|
||||
|
||||
private fun navigateToMainActivity() {
|
||||
findNavController().navigate(TvLoginFragmentDirections.actionLoginFragmentToNavigationHome())
|
||||
viewModel.doneNavigatingToMain()
|
||||
}
|
||||
}
|
|
@ -2,8 +2,7 @@ package dev.jdtech.jellyfin.viewmodels
|
|||
|
||||
import android.content.res.Resources
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -13,9 +12,7 @@ import dev.jdtech.jellyfin.api.JellyfinApi
|
|||
import dev.jdtech.jellyfin.database.Server
|
||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerInfo
|
||||
|
@ -32,100 +29,144 @@ constructor(
|
|||
private val jellyfinApi: JellyfinApi,
|
||||
private val database: ServerDatabaseDao
|
||||
) : ViewModel() {
|
||||
private val resources: Resources = application.resources
|
||||
|
||||
private val _navigateToLogin = MutableLiveData<Boolean>()
|
||||
val navigateToLogin: LiveData<Boolean> = _navigateToLogin
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Normal)
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
private val navigateToLogin = MutableSharedFlow<Boolean>()
|
||||
|
||||
sealed class UiState {
|
||||
object Normal : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String) : UiState()
|
||||
}
|
||||
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
fun onNavigateToLogin(scope: LifecycleCoroutineScope, collector: (Boolean) -> Unit) {
|
||||
scope.launch { navigateToLogin.collect { collector(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Run multiple check on the server before continuing:
|
||||
*
|
||||
* - Connect to server and check if it is a Jellyfin server
|
||||
* - Check if server is not already in Database
|
||||
*
|
||||
* @param inputValue Can be an ip address or hostname
|
||||
*/
|
||||
fun checkServer(inputValue: String, resources: Resources) {
|
||||
_error.value = null
|
||||
fun checkServer(inputValue: String) {
|
||||
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
|
||||
try {
|
||||
// Check if input value is not empty
|
||||
if (inputValue.isBlank()) {
|
||||
throw Exception(resources.getString(R.string.add_server_error_empty_address))
|
||||
}
|
||||
|
||||
val candidates = jellyfinApi.jellyfin.discovery.getAddressCandidates(inputValue)
|
||||
val recommended = jellyfinApi.jellyfin.discovery.getRecommendedServers(
|
||||
candidates,
|
||||
RecommendedServerInfoScore.OK
|
||||
)
|
||||
|
||||
// Check if any servers have been found
|
||||
if (recommended.toList().isNullOrEmpty()) {
|
||||
throw Exception(resources.getString(R.string.add_server_error_not_found))
|
||||
}
|
||||
val greatServers = mutableListOf<RecommendedServerInfo>()
|
||||
val goodServers = mutableListOf<RecommendedServerInfo>()
|
||||
val okServers = mutableListOf<RecommendedServerInfo>()
|
||||
|
||||
// Create separate flow of great, good and ok servers.
|
||||
val greatServers =
|
||||
recommended.filter { it.score == RecommendedServerInfoScore.GREAT }
|
||||
val goodServers = recommended.filter { it.score == RecommendedServerInfoScore.GOOD }
|
||||
val okServers = recommended.filter { it.score == RecommendedServerInfoScore.OK }
|
||||
|
||||
// Only allow connecting to great and good servers. Show toast of issues if good server
|
||||
val recommendedServer = if (greatServers.toList().isNotEmpty()) {
|
||||
greatServers.first()
|
||||
} else if (goodServers.toList().isNotEmpty()) {
|
||||
val issuesString = createIssuesString(goodServers.first(), resources)
|
||||
Toast.makeText(
|
||||
application,
|
||||
issuesString,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
goodServers.first()
|
||||
} else {
|
||||
val okServer = okServers.first()
|
||||
val issuesString = createIssuesString(okServer, resources)
|
||||
throw Exception(issuesString)
|
||||
}
|
||||
|
||||
jellyfinApi.apply {
|
||||
api.baseUrl = recommendedServer.address
|
||||
api.accessToken = null
|
||||
}
|
||||
|
||||
Timber.d("Remote server: ${recommendedServer.systemInfo.getOrNull()?.id}")
|
||||
|
||||
if (serverAlreadyInDatabase(recommendedServer.systemInfo.getOrNull()?.id)) {
|
||||
_error.value = resources.getString(R.string.add_server_error_already_added)
|
||||
_navigateToLogin.value = false
|
||||
} else {
|
||||
_error.value = null
|
||||
_navigateToLogin.value = true
|
||||
}
|
||||
recommended
|
||||
.onCompletion {
|
||||
if (greatServers.isNotEmpty()) {
|
||||
connectToServer(greatServers.first())
|
||||
} else if (goodServers.isNotEmpty()) {
|
||||
val issuesString = createIssuesString(goodServers.first())
|
||||
Toast.makeText(
|
||||
application,
|
||||
issuesString,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
connectToServer(goodServers.first())
|
||||
} else if (okServers.isNotEmpty()) {
|
||||
val okServer = okServers.first()
|
||||
val issuesString = createIssuesString(okServer)
|
||||
throw Exception(issuesString)
|
||||
} else {
|
||||
throw Exception(resources.getString(R.string.add_server_error_not_found))
|
||||
}
|
||||
}
|
||||
.collect { recommendedServerInfo ->
|
||||
when (recommendedServerInfo.score) {
|
||||
RecommendedServerInfoScore.GREAT -> greatServers.add(recommendedServerInfo)
|
||||
RecommendedServerInfoScore.GOOD -> goodServers.add(recommendedServerInfo)
|
||||
RecommendedServerInfoScore.OK -> okServers.add(recommendedServerInfo)
|
||||
RecommendedServerInfoScore.BAD -> Unit
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_navigateToLogin.value = false
|
||||
uiState.emit(
|
||||
UiState.Error(
|
||||
e.message ?: resources.getString(R.string.unknown_error)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun connectToServer(recommendedServerInfo: RecommendedServerInfo) {
|
||||
val serverId = recommendedServerInfo.systemInfo.getOrNull()?.id
|
||||
?: throw Exception(resources.getString(R.string.add_server_error_no_id))
|
||||
|
||||
Timber.d("Connecting to server: $serverId")
|
||||
|
||||
if (serverAlreadyInDatabase(serverId)) {
|
||||
throw Exception(resources.getString(R.string.add_server_error_already_added))
|
||||
}
|
||||
|
||||
jellyfinApi.apply {
|
||||
api.baseUrl = recommendedServerInfo.address
|
||||
api.accessToken = null
|
||||
}
|
||||
|
||||
uiState.emit(UiState.Normal)
|
||||
navigateToLogin.emit(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a presentable string of issues with a server
|
||||
*
|
||||
* @param server The server with issues
|
||||
* @return A presentable string of issues separated with \n
|
||||
*/
|
||||
private fun createIssuesString(server: RecommendedServerInfo, resources: Resources): String {
|
||||
private fun createIssuesString(server: RecommendedServerInfo): String {
|
||||
return server.issues.joinToString("\n") {
|
||||
when (it) {
|
||||
is RecommendedServerIssue.OutdatedServerVersion -> {
|
||||
String.format(resources.getString(R.string.add_server_error_outdated), it.version)
|
||||
String.format(
|
||||
resources.getString(R.string.add_server_error_outdated),
|
||||
it.version
|
||||
)
|
||||
}
|
||||
is RecommendedServerIssue.InvalidProductName -> {
|
||||
String.format(resources.getString(R.string.add_server_error_not_jellyfin), it.productName)
|
||||
String.format(
|
||||
resources.getString(R.string.add_server_error_not_jellyfin),
|
||||
it.productName
|
||||
)
|
||||
}
|
||||
is RecommendedServerIssue.UnsupportedServerVersion -> {
|
||||
String.format(resources.getString(R.string.add_server_error_version), it.version)
|
||||
String.format(
|
||||
resources.getString(R.string.add_server_error_version),
|
||||
it.version
|
||||
)
|
||||
}
|
||||
is RecommendedServerIssue.SlowResponse -> {
|
||||
String.format(resources.getString(R.string.add_server_error_slow), it.responseTime)
|
||||
String.format(
|
||||
resources.getString(R.string.add_server_error_slow),
|
||||
it.responseTime
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
resources.getString(R.string.unknown_error)
|
||||
|
@ -140,22 +181,12 @@ constructor(
|
|||
* @param id Server ID
|
||||
* @return True if server is already in database
|
||||
*/
|
||||
private suspend fun serverAlreadyInDatabase(id: String?): Boolean {
|
||||
val servers: List<Server>
|
||||
private suspend fun serverAlreadyInDatabase(id: String): Boolean {
|
||||
val server: Server?
|
||||
withContext(Dispatchers.IO) {
|
||||
servers = database.getAllServersSync()
|
||||
}
|
||||
for (server in servers) {
|
||||
Timber.d("Database server: ${server.id}")
|
||||
if (server.id == id) {
|
||||
Timber.w("Server already in the database")
|
||||
return true
|
||||
}
|
||||
server = database.get(id)
|
||||
}
|
||||
if (server != null) return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun onNavigateToLoginDone() {
|
||||
_navigateToLogin.value = false
|
||||
}
|
||||
}
|
|
@ -1,12 +1,18 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import androidx.lifecycle.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.BaseApplication
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||
import dev.jdtech.jellyfin.database.Server
|
||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.AuthenticateUserByName
|
||||
|
@ -18,17 +24,30 @@ import javax.inject.Inject
|
|||
class LoginViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
application: BaseApplication,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val jellyfinApi: JellyfinApi,
|
||||
private val database: ServerDatabaseDao
|
||||
) : ViewModel() {
|
||||
private val resources: Resources = application.resources
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Normal)
|
||||
|
||||
private val navigateToMain = MutableSharedFlow<Boolean>()
|
||||
|
||||
private val _navigateToMain = MutableLiveData<Boolean>()
|
||||
val navigateToMain: LiveData<Boolean> = _navigateToMain
|
||||
sealed class UiState {
|
||||
object Normal : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String) : UiState()
|
||||
}
|
||||
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
fun onNavigateToMain(scope: LifecycleCoroutineScope, collector: (Boolean) -> Unit) {
|
||||
scope.launch { navigateToMain.collect { collector(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a authentication request to the Jellyfin server
|
||||
|
@ -38,6 +57,8 @@ constructor(
|
|||
*/
|
||||
fun login(username: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
|
||||
try {
|
||||
val authenticationResult by jellyfinApi.userApi.authenticateUserByName(
|
||||
data = AuthenticateUserByName(
|
||||
|
@ -45,8 +66,9 @@ constructor(
|
|||
pw = password
|
||||
)
|
||||
)
|
||||
_error.value = null
|
||||
|
||||
val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo()
|
||||
|
||||
val server = Server(
|
||||
serverInfo.id!!,
|
||||
serverInfo.serverName!!,
|
||||
|
@ -55,18 +77,27 @@ constructor(
|
|||
authenticationResult.user?.name!!,
|
||||
authenticationResult.accessToken!!
|
||||
)
|
||||
|
||||
insert(server)
|
||||
|
||||
val spEdit = sharedPreferences.edit()
|
||||
spEdit.putString("selectedServer", server.id)
|
||||
spEdit.apply()
|
||||
|
||||
jellyfinApi.apply {
|
||||
api.accessToken = authenticationResult.accessToken
|
||||
userId = authenticationResult.user?.id
|
||||
}
|
||||
_navigateToMain.value = true
|
||||
|
||||
uiState.emit(UiState.Normal)
|
||||
navigateToMain.emit(true)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
val message =
|
||||
if (e.cause?.message?.contains("401") == true) resources.getString(R.string.login_error_wrong_username_password) else resources.getString(
|
||||
R.string.unknown_error
|
||||
)
|
||||
uiState.emit(UiState.Error(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,8 +112,4 @@ constructor(
|
|||
database.insert(server)
|
||||
}
|
||||
}
|
||||
|
||||
fun doneNavigatingToMain() {
|
||||
_navigateToMain.value = false
|
||||
}
|
||||
}
|
|
@ -11,87 +11,95 @@
|
|||
type="dev.jdtech.jellyfin.viewmodels.AddServerViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
tools:context=".fragments.AddServerFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_banner"
|
||||
android:layout_width="268dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_marginTop="64dp"
|
||||
android:contentDescription="@string/jellyfin_banner"
|
||||
android:src="@drawable/ic_banner"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<ImageView
|
||||
android:id="@+id/image_banner"
|
||||
android:layout_width="268dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_marginTop="64dp"
|
||||
android:contentDescription="@string/jellyfin_banner"
|
||||
android:src="@drawable/ic_banner"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="@dimen/setup_container_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/image_banner"
|
||||
app:layout_constraintVertical_bias="0.36">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_add_server"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="@dimen/setup_container_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/add_server"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:textColor="?android:textColorPrimary" />
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/image_banner"
|
||||
app:layout_constraintVertical_bias="0.36">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/edit_text_server_address_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/edit_text_server_address_hint"
|
||||
app:errorEnabled="true"
|
||||
app:startIconDrawable="@drawable/ic_server">
|
||||
<TextView
|
||||
android:id="@+id/text_add_server"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/add_server"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:textColor="?android:textColorPrimary" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_server_address"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/edit_text_server_address_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:singleLine="true" />
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="@string/edit_text_server_address_hint"
|
||||
app:errorEnabled="true"
|
||||
app:startIconDrawable="@drawable/ic_server">
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_server_address"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="textUri"
|
||||
android:singleLine="true" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_connect"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableStart="@drawable/ic_launcher_foreground"
|
||||
android:text="@string/button_connect" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:elevation="8dp"
|
||||
android:indeterminateTint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:visibility="invisible" />
|
||||
<Button
|
||||
android:id="@+id/button_connect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableStart="@drawable/ic_launcher_foreground"
|
||||
android:text="@string/button_connect" />
|
||||
|
||||
</RelativeLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:elevation="8dp"
|
||||
android:indeterminateTint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
|
|
|
@ -11,102 +11,111 @@
|
|||
type="dev.jdtech.jellyfin.viewmodels.LoginViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
tools:context=".fragments.LoginFragment">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_banner"
|
||||
android:layout_width="268dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_marginTop="64dp"
|
||||
android:contentDescription="@string/jellyfin_banner"
|
||||
android:src="@drawable/ic_banner"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="@dimen/setup_container_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/image_banner"
|
||||
app:layout_constraintVertical_bias="0.36">
|
||||
<ImageView
|
||||
android:id="@+id/image_banner"
|
||||
android:layout_width="268dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_marginTop="64dp"
|
||||
android:contentDescription="@string/jellyfin_banner"
|
||||
android:src="@drawable/ic_banner"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_login"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="@dimen/setup_container_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/login"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:textColor="?android:textColorPrimary" />
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/image_banner"
|
||||
app:layout_constraintVertical_bias="0.36">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/edit_text_username_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/edit_text_username_hint"
|
||||
app:startIconDrawable="@drawable/ic_user">
|
||||
<TextView
|
||||
android:id="@+id/text_login"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/login"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:textColor="?android:textColorPrimary" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_username"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/edit_text_username_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:inputType="text" />
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/edit_text_username_hint"
|
||||
app:startIconDrawable="@drawable/ic_user">
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:inputType="text" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/edit_text_password_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="@string/edit_text_password_hint"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:startIconDrawable="@drawable/ic_lock">
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_password"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/edit_text_password_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:inputType="textPassword" />
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="@string/edit_text_password_hint"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:startIconDrawable="@drawable/ic_lock">
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_login"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_login" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:elevation="8dp"
|
||||
android:indeterminateTint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:visibility="invisible" />
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/button_login"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_login" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:elevation="8dp"
|
||||
android:indeterminateTint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:visibility="invisible" />
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</layout>
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
<string name="add_server_error_already_added">Server already added</string>
|
||||
<string name="add_server_error_empty_address">Empty server address</string>
|
||||
<string name="add_server_error_not_found">Server not found</string>
|
||||
<string name="add_server_error_no_id">Server has no id, something seems to be wrong with the server</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="login_error_wrong_username_password">Wrong username or password</string>
|
||||
<string name="select_server">Select server</string>
|
||||
<string name="edit_text_server_address_hint">Server address</string>
|
||||
<string name="edit_text_username_hint">Username</string>
|
||||
|
|
Loading…
Reference in a new issue