Log in with Quick Connect (#234)
* Log in with Quick Connect * Clean up LoginViewModel * Cancel Quick Connect by tapping the button again * Make quickConnectJob private
This commit is contained in:
parent
76121925d7
commit
f107e79b72
5 changed files with 134 additions and 15 deletions
|
@ -1,10 +1,12 @@
|
||||||
package dev.jdtech.jellyfin.fragments
|
package dev.jdtech.jellyfin.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -19,6 +21,7 @@ import dev.jdtech.jellyfin.adapters.UserLoginListAdapter
|
||||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||||
import dev.jdtech.jellyfin.databinding.FragmentLoginBinding
|
import dev.jdtech.jellyfin.databinding.FragmentLoginBinding
|
||||||
import dev.jdtech.jellyfin.AppPreferences
|
import dev.jdtech.jellyfin.AppPreferences
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
|
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -66,6 +69,10 @@ class LoginFragment : Fragment() {
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.buttonQuickconnect.setOnClickListener {
|
||||||
|
viewModel.useQuickConnect()
|
||||||
|
}
|
||||||
|
|
||||||
binding.usersRecyclerView.adapter = UserLoginListAdapter { user ->
|
binding.usersRecyclerView.adapter = UserLoginListAdapter { user ->
|
||||||
(binding.editTextUsername as AppCompatEditText).setText(user.name)
|
(binding.editTextUsername as AppCompatEditText).setText(user.name)
|
||||||
(binding.editTextPassword as AppCompatEditText).requestFocus()
|
(binding.editTextPassword as AppCompatEditText).requestFocus()
|
||||||
|
@ -95,6 +102,32 @@ class LoginFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
viewModel.quickConnectUiState.collect { quickConnectUiState ->
|
||||||
|
when (quickConnectUiState) {
|
||||||
|
is LoginViewModel.QuickConnectUiState.Disabled -> {
|
||||||
|
binding.buttonQuickconnectLayout.isVisible = false
|
||||||
|
}
|
||||||
|
is LoginViewModel.QuickConnectUiState.Normal -> {
|
||||||
|
binding.buttonQuickconnectLayout.isVisible = true
|
||||||
|
binding.buttonQuickconnect.text = resources.getString(R.string.quick_connect)
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
requireActivity().theme.resolveAttribute(R.attr.colorPrimary, typedValue, true)
|
||||||
|
@ColorInt val textColor: Int = typedValue.data
|
||||||
|
binding.buttonQuickconnect.setTextColor(textColor)
|
||||||
|
binding.buttonQuickconnectProgress.isVisible = false
|
||||||
|
}
|
||||||
|
is LoginViewModel.QuickConnectUiState.Waiting -> {
|
||||||
|
binding.buttonQuickconnect.text = quickConnectUiState.code
|
||||||
|
binding.buttonQuickconnect.setTextColor(resources.getColor(android.R.color.white, requireActivity().theme))
|
||||||
|
binding.buttonQuickconnectProgress.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
viewModel.navigateToMain.collect {
|
viewModel.navigateToMain.collect {
|
||||||
|
|
|
@ -116,6 +116,31 @@
|
||||||
android:visibility="invisible" />
|
android:visibility="invisible" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/button_quickconnect_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_quickconnect"
|
||||||
|
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/quick_connect" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/button_quickconnect_progress"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:indeterminateTint="?attr/colorPrimary"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -12,13 +12,17 @@ import dev.jdtech.jellyfin.AppPreferences
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.Exception
|
import kotlin.Exception
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.authenticateWithQuickConnect
|
||||||
import org.jellyfin.sdk.model.api.AuthenticateUserByName
|
import org.jellyfin.sdk.model.api.AuthenticateUserByName
|
||||||
|
import org.jellyfin.sdk.model.api.AuthenticationResult
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LoginViewModel
|
class LoginViewModel
|
||||||
|
@ -32,9 +36,13 @@ constructor(
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
private val _usersState = MutableStateFlow<UsersState>(UsersState.Loading)
|
private val _usersState = MutableStateFlow<UsersState>(UsersState.Loading)
|
||||||
val usersState = _usersState.asStateFlow()
|
val usersState = _usersState.asStateFlow()
|
||||||
|
private val _quickConnectUiState = MutableStateFlow<QuickConnectUiState>(QuickConnectUiState.Disabled)
|
||||||
|
val quickConnectUiState = _quickConnectUiState.asStateFlow()
|
||||||
private val _navigateToMain = MutableSharedFlow<Boolean>()
|
private val _navigateToMain = MutableSharedFlow<Boolean>()
|
||||||
val navigateToMain = _navigateToMain.asSharedFlow()
|
val navigateToMain = _navigateToMain.asSharedFlow()
|
||||||
|
|
||||||
|
private var quickConnectJob: Job? = null
|
||||||
|
|
||||||
sealed class UiState {
|
sealed class UiState {
|
||||||
object Normal : UiState()
|
object Normal : UiState()
|
||||||
object Loading : UiState()
|
object Loading : UiState()
|
||||||
|
@ -46,8 +54,15 @@ constructor(
|
||||||
data class Users(val users: List<User>) : UsersState()
|
data class Users(val users: List<User>) : UsersState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class QuickConnectUiState {
|
||||||
|
object Disabled : QuickConnectUiState()
|
||||||
|
object Normal : QuickConnectUiState()
|
||||||
|
data class Waiting(val code: String) : QuickConnectUiState()
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadPublicUsers()
|
loadPublicUsers()
|
||||||
|
loadQuickConnectAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPublicUsers() {
|
private fun loadPublicUsers() {
|
||||||
|
@ -74,6 +89,17 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadQuickConnectAvailable() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val isEnabled by jellyfinApi.quickConnectApi.getEnabled()
|
||||||
|
if (isEnabled) {
|
||||||
|
_quickConnectUiState.emit(QuickConnectUiState.Normal)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a authentication request to the Jellyfin server
|
* Send a authentication request to the Jellyfin server
|
||||||
*
|
*
|
||||||
|
@ -92,6 +118,49 @@ constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
saveAuthenticationResult(authenticationResult)
|
||||||
|
|
||||||
|
_uiState.emit(UiState.Normal)
|
||||||
|
_navigateToMain.emit(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val message =
|
||||||
|
if (e.message?.contains("401") == true) UiText.StringResource(R.string.login_error_wrong_username_password) else UiText.StringResource(
|
||||||
|
R.string.unknown_error
|
||||||
|
)
|
||||||
|
_uiState.emit(UiState.Error(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun useQuickConnect() {
|
||||||
|
if (quickConnectJob != null && quickConnectJob!!.isActive) {
|
||||||
|
quickConnectJob!!.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
quickConnectJob = viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
var quickConnectState = jellyfinApi.quickConnectApi.initiate().content
|
||||||
|
_quickConnectUiState.emit(QuickConnectUiState.Waiting(quickConnectState.code))
|
||||||
|
|
||||||
|
while (!quickConnectState.authenticated) {
|
||||||
|
quickConnectState = jellyfinApi.quickConnectApi.connect(quickConnectState.secret).content
|
||||||
|
delay(5000L)
|
||||||
|
}
|
||||||
|
val authenticationResult by jellyfinApi.userApi.authenticateWithQuickConnect(
|
||||||
|
secret = quickConnectState.secret
|
||||||
|
)
|
||||||
|
|
||||||
|
saveAuthenticationResult(authenticationResult)
|
||||||
|
|
||||||
|
_quickConnectUiState.emit(QuickConnectUiState.Normal)
|
||||||
|
_navigateToMain.emit(true)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
_quickConnectUiState.emit(QuickConnectUiState.Normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveAuthenticationResult(authenticationResult: AuthenticationResult) {
|
||||||
val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo()
|
val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo()
|
||||||
|
|
||||||
val user = User(
|
val user = User(
|
||||||
|
@ -107,17 +176,6 @@ constructor(
|
||||||
api.accessToken = authenticationResult.accessToken
|
api.accessToken = authenticationResult.accessToken
|
||||||
userId = authenticationResult.user?.id
|
userId = authenticationResult.user?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
_uiState.emit(UiState.Normal)
|
|
||||||
_navigateToMain.emit(true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val message =
|
|
||||||
if (e.message?.contains("401") == true) UiText.StringResource(R.string.login_error_wrong_username_password) else UiText.StringResource(
|
|
||||||
R.string.unknown_error
|
|
||||||
)
|
|
||||||
_uiState.emit(UiState.Error(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun insertUser(serverId: String, user: User) {
|
private suspend fun insertUser(serverId: String, user: User) {
|
||||||
|
|
|
@ -147,4 +147,5 @@
|
||||||
<string name="add_address">Add address</string>
|
<string name="add_address">Add address</string>
|
||||||
<string name="add_server_address">Add server address</string>
|
<string name="add_server_address">Add server address</string>
|
||||||
<string name="add">Add</string>
|
<string name="add">Add</string>
|
||||||
|
<string name="quick_connect">Quick Connect</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,6 +9,7 @@ import org.jellyfin.sdk.api.client.extensions.devicesApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.sessionApi
|
import org.jellyfin.sdk.api.client.extensions.sessionApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.systemApi
|
import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
|
@ -57,6 +58,7 @@ class JellyfinApi(
|
||||||
val videosApi = api.videosApi
|
val videosApi = api.videosApi
|
||||||
val mediaInfoApi = api.mediaInfoApi
|
val mediaInfoApi = api.mediaInfoApi
|
||||||
val playStateApi = api.playStateApi
|
val playStateApi = api.playStateApi
|
||||||
|
val quickConnectApi = api.quickConnectApi
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|
Loading…
Reference in a new issue