diff --git a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt index 61876c10..947cbc93 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt @@ -12,6 +12,7 @@ import dev.jdtech.jellyfin.adapters.ServerGridAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.database.Server +import dev.jdtech.jellyfin.models.User import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemPerson @@ -57,7 +58,7 @@ fun bindItemBackdropById(imageView: ImageView, itemId: UUID) { @BindingAdapter("personImage") fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) { imageView - .loadImage("/items/${person.id}/Images/${ImageType.PRIMARY}", R.drawable.person_placeholder) + .loadImage("/items/${person.id}/Images/${ImageType.PRIMARY}", placeholderId = R.drawable.person_placeholder) .posterDescription(person.name) } @@ -104,14 +105,21 @@ fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) { imageView.loadImage("/items/${seasonId}/Images/${ImageType.PRIMARY}") } -private fun ImageView.loadImage(url: String, @DrawableRes errorPlaceHolderId: Int? = null): View { +@BindingAdapter("userImage") +fun bindUserImage(imageView: ImageView, user: User) { + imageView + .loadImage("/users/${user.id}/Images/${ImageType.PRIMARY}", placeholderId = R.drawable.user_placeholder) + .posterDescription(user.name) +} + +private fun ImageView.loadImage(url: String, @DrawableRes placeholderId: Int = R.color.neutral_800, @DrawableRes errorPlaceHolderId: Int? = null): View { val api = JellyfinApi.getInstance(context.applicationContext) Glide .with(context) .load("${api.api.baseUrl}$url") .transition(DrawableTransitionOptions.withCrossFade()) - .placeholder(R.color.neutral_800) + .placeholder(placeholderId) .error(errorPlaceHolderId) .into(this) diff --git a/app/src/main/java/dev/jdtech/jellyfin/adapters/UserListAdapter.kt b/app/src/main/java/dev/jdtech/jellyfin/adapters/UserListAdapter.kt new file mode 100644 index 00000000..ec9023e4 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/adapters/UserListAdapter.kt @@ -0,0 +1,50 @@ +package dev.jdtech.jellyfin.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import dev.jdtech.jellyfin.databinding.UserItemBinding +import dev.jdtech.jellyfin.models.User + +class UserListAdapter( + private val clickListener: (user: User) -> Unit +) : ListAdapter(DiffCallback) { + class UserViewHolder(private var binding: UserItemBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(user: User) { + binding.user = user + binding.executePendingBindings() + } + } + + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { + return oldItem == newItem + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): UserViewHolder { + return UserViewHolder( + UserItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: UserViewHolder, position: Int) { + val user = getItem(position) + holder.itemView.setOnClickListener { clickListener(user) } + holder.bind(user) + } +} \ 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 af712d24..23bc82a5 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt @@ -17,6 +17,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.adapters.UserListAdapter import dev.jdtech.jellyfin.databinding.FragmentLoginBinding import dev.jdtech.jellyfin.viewmodels.LoginViewModel import kotlinx.coroutines.launch @@ -51,11 +52,16 @@ class LoginFragment : Fragment() { login() } + binding.usersRecyclerView.adapter = UserListAdapter { user -> + (binding.editTextUsername as AppCompatEditText).setText(user.name) + (binding.editTextPassword as AppCompatEditText).requestFocus() + } + viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { uiState -> Timber.d("$uiState") - when(uiState) { + when (uiState) { is LoginViewModel.UiState.Normal -> bindUiStateNormal() is LoginViewModel.UiState.Error -> bindUiStateError(uiState) is LoginViewModel.UiState.Loading -> bindUiStateLoading() @@ -64,6 +70,17 @@ class LoginFragment : Fragment() { } } + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.usersState.collect { usersState -> + when (usersState) { + is LoginViewModel.UsersState.Loading -> Unit + is LoginViewModel.UsersState.Users -> bindUsersStateUsers(usersState) + } + } + } + } + viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.navigateToMain.collect { @@ -102,6 +119,16 @@ class LoginFragment : Fragment() { } } + private fun bindUsersStateUsers(usersState: LoginViewModel.UsersState.Users) { + val users = usersState.users + if (users.isEmpty()) { + binding.usersRecyclerView.isVisible = false + } else { + binding.usersRecyclerView.isVisible = true + (binding.usersRecyclerView.adapter as UserListAdapter).submitList(users) + } + } + private fun login() { val username = (binding.editTextUsername as AppCompatEditText).text.toString() val password = (binding.editTextPassword as AppCompatEditText).text.toString() diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/User.kt b/app/src/main/java/dev/jdtech/jellyfin/models/User.kt new file mode 100644 index 00000000..13b9ac08 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/models/User.kt @@ -0,0 +1,8 @@ +package dev.jdtech.jellyfin.models + +import java.util.UUID + +data class User( + val id: UUID, + val name: String +) \ 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 ece3d085..3d26eb93 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt @@ -9,6 +9,7 @@ import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.ServerDatabaseDao +import dev.jdtech.jellyfin.models.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -18,8 +19,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.model.api.AuthenticateUserByName import timber.log.Timber -import java.lang.Exception import javax.inject.Inject +import kotlin.Exception @HiltViewModel class LoginViewModel @@ -34,6 +35,8 @@ constructor( private val _uiState = MutableStateFlow(UiState.Normal) val uiState = _uiState.asStateFlow() + private val _usersState = MutableStateFlow(UsersState.Loading) + val usersState = _usersState.asStateFlow() private val _navigateToMain = MutableSharedFlow() val navigateToMain = _navigateToMain.asSharedFlow() @@ -43,6 +46,28 @@ constructor( data class Error(val message: String) : UiState() } + sealed class UsersState { + object Loading : UsersState() + data class Users(val users: List) : UsersState() + } + + init { + loadPublicUsers() + } + + private fun loadPublicUsers() { + viewModelScope.launch { + _usersState.emit(UsersState.Loading) + try { + val publicUsers by jellyfinApi.userApi.getPublicUsers() + val users = publicUsers.map { User(it.id, it.name.orEmpty()) } + _usersState.emit(UsersState.Users(users)) + } catch (e: Exception) { + _usersState.emit(UsersState.Users(emptyList())) + } + } + } + /** * Send a authentication request to the Jellyfin server * diff --git a/app/src/main/res/drawable-television/user_placeholder.xml b/app/src/main/res/drawable-television/user_placeholder.xml new file mode 100644 index 00000000..90c2f6cf --- /dev/null +++ b/app/src/main/res/drawable-television/user_placeholder.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_user_color_on_primary.xml b/app/src/main/res/drawable/ic_user_color_on_primary.xml new file mode 100644 index 00000000..b41545e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_color_on_primary.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/user_placeholder.xml b/app/src/main/res/drawable/user_placeholder.xml new file mode 100644 index 00000000..b164977e --- /dev/null +++ b/app/src/main/res/drawable/user_placeholder.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-television/fragment_login.xml b/app/src/main/res/layout-television/fragment_login.xml index e3c115c7..980bd569 100644 --- a/app/src/main/res/layout-television/fragment_login.xml +++ b/app/src/main/res/layout-television/fragment_login.xml @@ -26,8 +26,6 @@ android:id="@+id/linearLayout" android:layout_width="@dimen/setup_container_width" android:layout_height="wrap_content" - android:layout_marginStart="24dp" - android:layout_marginEnd="24dp" android:orientation="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -39,15 +37,31 @@ android:id="@+id/text_login" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" android:layout_marginBottom="32dp" android:text="@string/login" android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" android:textColor="?android:textColorPrimary" /> + + + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp">