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:
Jarne Demeulemeester 2023-01-14 18:21:42 +01:00 committed by GitHub
parent 76121925d7
commit f107e79b72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 15 deletions

View file

@ -1,10 +1,12 @@
package dev.jdtech.jellyfin.fragments
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.annotation.ColorInt
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.isVisible
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.databinding.FragmentLoginBinding
import dev.jdtech.jellyfin.AppPreferences
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
import javax.inject.Inject
import kotlinx.coroutines.launch
@ -66,6 +69,10 @@ class LoginFragment : Fragment() {
login()
}
binding.buttonQuickconnect.setOnClickListener {
viewModel.useQuickConnect()
}
binding.usersRecyclerView.adapter = UserLoginListAdapter { user ->
(binding.editTextUsername as AppCompatEditText).setText(user.name)
(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.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navigateToMain.collect {

View file

@ -116,6 +116,31 @@
android:visibility="invisible" />
</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>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -12,13 +12,17 @@ import dev.jdtech.jellyfin.AppPreferences
import javax.inject.Inject
import kotlin.Exception
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
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.AuthenticationResult
@HiltViewModel
class LoginViewModel
@ -32,9 +36,13 @@ constructor(
val uiState = _uiState.asStateFlow()
private val _usersState = MutableStateFlow<UsersState>(UsersState.Loading)
val usersState = _usersState.asStateFlow()
private val _quickConnectUiState = MutableStateFlow<QuickConnectUiState>(QuickConnectUiState.Disabled)
val quickConnectUiState = _quickConnectUiState.asStateFlow()
private val _navigateToMain = MutableSharedFlow<Boolean>()
val navigateToMain = _navigateToMain.asSharedFlow()
private var quickConnectJob: Job? = null
sealed class UiState {
object Normal : UiState()
object Loading : UiState()
@ -46,8 +54,15 @@ constructor(
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 {
loadPublicUsers()
loadQuickConnectAvailable()
}
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
*
@ -92,21 +118,7 @@ constructor(
)
)
val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo()
val user = User(
id = authenticationResult.user!!.id,
name = authenticationResult.user!!.name!!,
serverId = serverInfo.id!!,
accessToken = authenticationResult.accessToken!!
)
insertUser(appPreferences.currentServer!!, user)
jellyfinApi.apply {
api.accessToken = authenticationResult.accessToken
userId = authenticationResult.user?.id
}
saveAuthenticationResult(authenticationResult)
_uiState.emit(UiState.Normal)
_navigateToMain.emit(true)
@ -120,6 +132,52 @@ constructor(
}
}
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 user = User(
id = authenticationResult.user!!.id,
name = authenticationResult.user!!.name!!,
serverId = serverInfo.id!!,
accessToken = authenticationResult.accessToken!!
)
insertUser(appPreferences.currentServer!!, user)
jellyfinApi.apply {
api.accessToken = authenticationResult.accessToken
userId = authenticationResult.user?.id
}
}
private suspend fun insertUser(serverId: String, user: User) {
withContext(Dispatchers.IO) {
database.insertUser(user)

View file

@ -147,4 +147,5 @@
<string name="add_address">Add address</string>
<string name="add_server_address">Add server address</string>
<string name="add">Add</string>
<string name="quick_connect">Quick Connect</string>
</resources>

View file

@ -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.mediaInfoApi
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.systemApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
@ -57,6 +58,7 @@ class JellyfinApi(
val videosApi = api.videosApi
val mediaInfoApi = api.mediaInfoApi
val playStateApi = api.playStateApi
val quickConnectApi = api.quickConnectApi
companion object {
@Volatile