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:
Jarne Demeulemeester 2021-11-14 18:20:19 +01:00 committed by GitHub
parent 44d0a34539
commit 98cb038c24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 516 additions and 294 deletions

View file

@ -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" />

View file

@ -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()

View file

@ -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

View file

@ -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, {
binding.buttonConnect.setOnClickListener {
connectToServer()
}
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.editTextServerAddressLayout.error = it
})
}
}
}
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()
}
}

View file

@ -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, {
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()
}
}

View file

@ -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)
@ -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(

View file

@ -4,13 +4,18 @@ 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() {
@ -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, {
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()
}
}

View file

@ -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, {
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()
}
}

View file

@ -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,78 +29,110 @@ 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)
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()
goodServers.first()
} else {
connectToServer(goodServers.first())
} else if (okServers.isNotEmpty()) {
val okServer = okServers.first()
val issuesString = createIssuesString(okServer, resources)
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) {
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 = recommendedServer.address
api.baseUrl = recommendedServerInfo.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
}
} catch (e: Exception) {
Timber.e(e)
_error.value = e.message
_navigateToLogin.value = false
}
}
uiState.emit(UiState.Normal)
navigateToLogin.emit(true)
}
/**
@ -112,20 +141,32 @@ constructor(
* @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
}
}

View file

@ -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
}
}

View file

@ -11,11 +11,15 @@
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"
@ -28,7 +32,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="@dimen/setup_container_width"
@ -65,6 +68,7 @@
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" />
@ -93,5 +97,9 @@
</RelativeLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</layout>

View file

@ -11,11 +11,16 @@
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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/image_banner"
android:layout_width="268dp"
@ -82,6 +87,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:imeOptions="actionGo"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
@ -105,8 +111,11 @@
android:padding="8dp"
android:visibility="invisible" />
</RelativeLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</layout>

View file

@ -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>