Load views and latest items on home screen

Needs a lot of optimization and cleanup
This commit is contained in:
Jarne Demeulemeester 2021-06-13 21:26:28 +02:00
parent 93518f67ee
commit f276a26d7d
No known key found for this signature in database
GPG key ID: B61B7B150DB6A6D2
14 changed files with 228 additions and 53 deletions

View file

@ -66,6 +66,10 @@ dependencies {
// Jellyfin // Jellyfin
implementation "org.jellyfin.sdk:jellyfin-platform-android:$jellyfin_version" implementation "org.jellyfin.sdk:jellyfin-platform-android:$jellyfin_version"
// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
// Testing // Testing
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'

View file

@ -1,10 +1,17 @@
package dev.jdtech.jellyfin package dev.jdtech.jellyfin
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.widget.ImageView
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.adapters.ServerGridAdapter import dev.jdtech.jellyfin.adapters.ServerGridAdapter
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.adapters.ViewListAdapter import dev.jdtech.jellyfin.adapters.ViewListAdapter
import dev.jdtech.jellyfin.models.View
import dev.jdtech.jellyfin.models.ViewItem
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@BindingAdapter("servers") @BindingAdapter("servers")
@ -14,7 +21,22 @@ fun bindServers(recyclerView: RecyclerView, data: List<Server>?) {
} }
@BindingAdapter("views") @BindingAdapter("views")
fun bindViews(recyclerView: RecyclerView, data: List<BaseItemDto>?) { fun bindViews(recyclerView: RecyclerView, data: List<View>?) {
val adapter = recyclerView.adapter as ViewListAdapter val adapter = recyclerView.adapter as ViewListAdapter
adapter.submitList(data) adapter.submitList(data)
}
@BindingAdapter("items")
fun bindItems(recyclerView: RecyclerView, data: List<ViewItem>?) {
val adapter = recyclerView.adapter as ViewItemListAdapter
adapter.submitList(data)
}
@BindingAdapter("itemImage")
fun bindItemImage(imageView: ImageView, item: ViewItem) {
Glide
.with(imageView.context)
.load(item.primaryImageUrl)
.placeholder(ColorDrawable(Color.GRAY))
.into(imageView)
} }

View file

@ -0,0 +1,38 @@
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.BaseItemBinding
import dev.jdtech.jellyfin.models.ViewItem
import org.jellyfin.sdk.model.api.BaseItemDto
class ViewItemListAdapter : ListAdapter<ViewItem, ViewItemListAdapter.ItemViewHolder>(DiffCallback) {
class ItemViewHolder(private var binding: BaseItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(view: ViewItem) {
binding.item = view
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<ViewItem>() {
override fun areItemsTheSame(oldItem: ViewItem, newItem: ViewItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ViewItem, newItem: ViewItem): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(BaseItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
}

View file

@ -6,31 +6,32 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.databinding.ViewItemBinding import dev.jdtech.jellyfin.databinding.ViewItemBinding
import org.jellyfin.sdk.model.api.BaseItemDto import dev.jdtech.jellyfin.models.View
class ViewListAdapter : ListAdapter<BaseItemDto, ViewListAdapter.ViewHolder>(DiffCallback) { class ViewListAdapter : ListAdapter<View, ViewListAdapter.ViewViewHolder>(DiffCallback) {
class ViewHolder(private var binding: ViewItemBinding) : RecyclerView.ViewHolder(binding.root) { class ViewViewHolder(private var binding: ViewItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(view: BaseItemDto) { fun bind(view: View) {
binding.view = view binding.view = view
binding.itemsRecyclerView.adapter = ViewItemListAdapter()
binding.executePendingBindings() binding.executePendingBindings()
} }
} }
companion object DiffCallback : DiffUtil.ItemCallback<BaseItemDto>() { companion object DiffCallback : DiffUtil.ItemCallback<View>() {
override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { override fun areItemsTheSame(oldItem: View, newItem: View): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id
} }
override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { override fun areContentsTheSame(oldItem: View, newItem: View): Boolean {
return oldItem == newItem return oldItem == newItem
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewViewHolder {
return ViewHolder(ViewItemBinding.inflate(LayoutInflater.from(parent.context))) return ViewViewHolder(ViewItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewViewHolder, position: Int) {
val view = getItem(position) val view = getItem(position)
holder.bind(view) holder.bind(view)
} }

View file

@ -5,9 +5,7 @@ import android.util.Log
import dev.jdtech.jellyfin.BuildConfig import dev.jdtech.jellyfin.BuildConfig
import org.jellyfin.sdk.Jellyfin import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.android import org.jellyfin.sdk.android
import org.jellyfin.sdk.api.operations.SystemApi import org.jellyfin.sdk.api.operations.*
import org.jellyfin.sdk.api.operations.UserApi
import org.jellyfin.sdk.api.operations.UserViewsApi
import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.ClientInfo
import java.util.* import java.util.*
@ -31,6 +29,8 @@ class JellyfinApi(context: Context, baseUrl: String) {
val systemApi = SystemApi(api) val systemApi = SystemApi(api)
val userApi = UserApi(api) val userApi = UserApi(api)
val viewsApi = UserViewsApi(api) val viewsApi = UserViewsApi(api)
val itemsApi = ItemsApi(api)
val userLibraryApi = UserLibraryApi(api)
init { init {
Log.i("JellyfinApi", "Constructor called!") Log.i("JellyfinApi", "Constructor called!")

View file

@ -0,0 +1,10 @@
package dev.jdtech.jellyfin.models
import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.*
data class View(
val id: UUID,
val name: String?,
var items: List<ViewItem>? = null
)

View file

@ -0,0 +1,9 @@
package dev.jdtech.jellyfin.models
import java.util.*
data class ViewItem(
val id: UUID,
val name: String?,
val primaryImageUrl: String
)

View file

@ -1,11 +1,11 @@
package dev.jdtech.jellyfin.viewmodels package dev.jdtech.jellyfin.viewmodels
import android.app.Application import android.app.Application
import androidx.lifecycle.LiveData import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.models.View
import dev.jdtech.jellyfin.models.ViewItem
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -14,14 +14,50 @@ class HomeViewModel(
) : ViewModel() { ) : ViewModel() {
private val jellyfinApi = JellyfinApi.getInstance(application, "") private val jellyfinApi = JellyfinApi.getInstance(application, "")
private val _views = MutableLiveData<List<BaseItemDto>>() private val _views = MutableLiveData<List<View>>()
val views: LiveData<List<BaseItemDto>> = _views val views: LiveData<List<View>> = _views
private val _items = MutableLiveData<List<BaseItemDto>>()
val items: LiveData<List<BaseItemDto>> = _items
init { init {
viewModelScope.launch { viewModelScope.launch {
val views: MutableList<View> = mutableListOf()
val result by jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!) val result by jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!)
_views.value = result.items for (view in result.items!!) {
val items: MutableList<ViewItem> = mutableListOf()
val resultItems by jellyfinApi.userLibraryApi.getLatestMedia(jellyfinApi.userId!!, parentId = view.id)
if (resultItems.isEmpty()) continue
val v = view.toView()
for (item in resultItems) {
val i = jellyfinApi.api.baseUrl?.let { item.toViewItem(it) }
if (i != null) {
items.add(i)
}
}
v.items = items
views.add(v)
}
_views.value = views
} }
} }
} }
private fun BaseItemDto.toViewItem(baseUrl: String) : ViewItem {
return ViewItem(
id = id,
name = name,
primaryImageUrl = baseUrl.plus("/items/${id}/Images/Primary")
)
}
private fun BaseItemDto.toView() : View {
return View(
id = id,
name = name
)
}

View file

@ -37,7 +37,7 @@ class ServerSelectViewModel(
} }
fun connectToServer(server: Server) { fun connectToServer(server: Server) {
JellyfinApi.getInstance(application, server.address).apply { JellyfinApi.newInstance(application, server.address).apply {
api.accessToken = server.accessToken api.accessToken = server.accessToken
userId = UUID.fromString(server.userId) userId = UUID.fromString(server.userId)
} }

View file

@ -2,34 +2,34 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:defaultNavHost="true" android:paddingTop="?attr/actionBarSize">
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/main_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout> <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/main_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="dev.jdtech.jellyfin.models.ViewItem" />
</data>
<LinearLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:orientation="vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/item_image"
android:layout_width="match_parent"
android:layout_height="220dp"
android:layout_marginBottom="8dp"
app:itemImage="@{item}"
app:shapeAppearanceOverlay="@style/roundedImageView" />
<TextView
android:id="@+id/item_name"
style="@style/text_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@{item.name}"
tools:text="Movie title" />
</LinearLayout>
</layout>

View file

@ -17,7 +17,7 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/views_recycler_view" android:id="@+id/views_recycler_view"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View file

@ -7,18 +7,20 @@
<variable <variable
name="view" name="view"
type="org.jellyfin.sdk.model.api.BaseItemDto" /> type="dev.jdtech.jellyfin.models.View" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:layout_marginBottom="42dp">
<TextView <TextView
android:id="@+id/view_name" android:id="@+id/view_name"
style="@style/text_title" style="@style/text_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="@{view.name}" android:text="@{view.name}"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -29,10 +31,22 @@
style="@style/text_regular" style="@style/text_regular"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:text="View all" android:text="View all"
app:layout_constraintBaseline_toBaselineOf="@id/view_name" app:layout_constraintBaseline_toBaselineOf="@id/view_name"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/items_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal"
app:items="@{view.items}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/view_name"
tools:listitem="@layout/base_item" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View file

@ -26,4 +26,9 @@
<item name="android:layout_height">48dp</item> <item name="android:layout_height">48dp</item>
<item name="android:background">@drawable/button_setup_background</item> <item name="android:background">@drawable/button_setup_background</item>
</style> </style>
<style name="roundedImageView">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">10dp</item>
</style>
</resources> </resources>