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

View file

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

View file

@ -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,21 +118,7 @@ constructor(
) )
) )
val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo() saveAuthenticationResult(authenticationResult)
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
}
_uiState.emit(UiState.Normal) _uiState.emit(UiState.Normal)
_navigateToMain.emit(true) _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) { private suspend fun insertUser(serverId: String, user: User) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
database.insertUser(user) database.insertUser(user)

View file

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

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