diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d0528a2..8daaf9e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,7 +33,7 @@ android:name=".MainActivity" android:exported="true" android:theme="@style/Theme.FindroidSplashScreen" - android:windowSoftInputMode="adjustPan"> + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt b/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt index f7f3efef..9f3b0a98 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt @@ -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() diff --git a/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt b/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt index df5813c6..b86b45b7 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt @@ -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 diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/AddServerFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/AddServerFragment.kt index b94a2b97..ee263d1e 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/AddServerFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/AddServerFragment.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt index 61d9e6aa..26542db7 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt index 768354a1..c9ffcb5e 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt @@ -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( diff --git a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvAddServerFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvAddServerFragment.kt index a46f0397..833222ea 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvAddServerFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvAddServerFragment.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvLoginFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvLoginFragment.kt index d7d9ad72..7f12df00 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvLoginFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvLoginFragment.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt index 4fa9377e..8212cc0b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt @@ -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() - val navigateToLogin: LiveData = _navigateToLogin + private val uiState = MutableStateFlow(UiState.Normal) - private val _error = MutableLiveData() - val error: LiveData = _error + private val navigateToLogin = MutableSharedFlow() + + 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() + val goodServers = mutableListOf() + val okServers = mutableListOf() - // 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 + 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 - } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt index 0d9a4185..83878a09 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt @@ -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() - val error: LiveData = _error + private val uiState = MutableStateFlow(UiState.Normal) + private val navigateToMain = MutableSharedFlow() - private val _navigateToMain = MutableLiveData() - val navigateToMain: LiveData = _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 - } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_server.xml b/app/src/main/res/layout/fragment_add_server.xml index 9ce9a579..6709eced 100644 --- a/app/src/main/res/layout/fragment_add_server.xml +++ b/app/src/main/res/layout/fragment_add_server.xml @@ -11,87 +11,95 @@ type="dev.jdtech.jellyfin.viewmodels.AddServerViewModel" /> - + - + - - - - + 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"> - + - + android:layout_marginBottom="8dp" + android:hint="@string/edit_text_server_address_hint" + app:errorEnabled="true" + app:startIconDrawable="@drawable/ic_server"> - + - + -