Add image caching (#65)
* Add caching to settings with ability to choose cache size * Remove unused parameter from Api * Add glide module for cache setup * Clean up image handling in adapters * Move caching to it's own category Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
parent
e259c405bb
commit
94b3790560
8 changed files with 155 additions and 106 deletions
|
@ -1,11 +1,22 @@
|
|||
package dev.jdtech.jellyfin
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import dev.jdtech.jellyfin.adapters.*
|
||||
import dev.jdtech.jellyfin.adapters.CollectionListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.DownloadsListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.EpisodeItem
|
||||
import dev.jdtech.jellyfin.adapters.EpisodeListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.FavoritesListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.ServerGridAdapter
|
||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.ViewListAdapter
|
||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||
import dev.jdtech.jellyfin.database.Server
|
||||
import dev.jdtech.jellyfin.models.DownloadSection
|
||||
|
@ -35,44 +46,26 @@ fun bindItems(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
|||
|
||||
@BindingAdapter("itemImage")
|
||||
fun bindItemImage(imageView: ImageView, item: BaseItemDto) {
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
val itemId =
|
||||
if (item.type == "Episode" || item.type == "Season" && item.imageTags.isNullOrEmpty()) item.seriesId else item.id
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/${ImageType.PRIMARY}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
|
||||
imageView.contentDescription = "${item.name} poster"
|
||||
imageView
|
||||
.loadImage("/items/$itemId/Images/${ImageType.PRIMARY}")
|
||||
.posterDescription(item.name)
|
||||
}
|
||||
|
||||
@BindingAdapter("itemBackdropImage")
|
||||
fun bindItemBackdropImage(imageView: ImageView, item: BaseItemDto?) {
|
||||
if (item == null) return
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/${ImageType.BACKDROP}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(imageView)
|
||||
|
||||
imageView.contentDescription = "${item.name} backdrop"
|
||||
imageView
|
||||
.loadImage("/items/${item.id}/Images/${ImageType.BACKDROP}")
|
||||
.backdropDescription(item.name)
|
||||
}
|
||||
|
||||
@BindingAdapter("itemBackdropById")
|
||||
fun bindItemBackdropById(imageView: ImageView, itemId: UUID) {
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/${ImageType.BACKDROP}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(imageView)
|
||||
imageView.loadImage("/items/$itemId/ MediaStore.Images /${ImageType.BACKDROP}")
|
||||
}
|
||||
|
||||
@BindingAdapter("collections")
|
||||
|
@ -89,17 +82,9 @@ fun bindPeople(recyclerView: RecyclerView, data: List<BaseItemPerson>?) {
|
|||
|
||||
@BindingAdapter("personImage")
|
||||
fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) {
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${person.id}/Images/${ImageType.PRIMARY}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.error(R.drawable.person_placeholder)
|
||||
.into(imageView)
|
||||
|
||||
imageView.contentDescription = "${person.name} poster"
|
||||
imageView
|
||||
.loadImage("/items/${person.id}/Images/${ImageType.PRIMARY}")
|
||||
.posterDescription(person.name)
|
||||
}
|
||||
|
||||
@BindingAdapter("episodes")
|
||||
|
@ -118,8 +103,6 @@ fun bindHomeEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
|||
fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) {
|
||||
if (episode == null) return
|
||||
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
var imageItemId = episode.id
|
||||
var imageType = ImageType.PRIMARY
|
||||
|
||||
|
@ -143,26 +126,14 @@ fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) {
|
|||
}
|
||||
}
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${imageItemId}/Images/$imageType"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
|
||||
imageView.contentDescription = "${episode.name} poster"
|
||||
imageView
|
||||
.loadImage("/items/${imageItemId}/Images/$imageType")
|
||||
.posterDescription(episode.name)
|
||||
}
|
||||
|
||||
@BindingAdapter("seasonPoster")
|
||||
fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) {
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${seasonId}/Images/${ImageType.PRIMARY}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
imageView.loadImage("/items/${seasonId}/Images/${ImageType.PRIMARY}")
|
||||
}
|
||||
|
||||
@BindingAdapter("favoriteSections")
|
||||
|
@ -175,4 +146,25 @@ fun bindFavoriteSections(recyclerView: RecyclerView, data: List<FavoriteSection>
|
|||
fun bindDownloadSections(recyclerView: RecyclerView, data: List<DownloadSection>?) {
|
||||
val adapter = recyclerView.adapter as DownloadsListAdapter
|
||||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
private fun ImageView.loadImage(url: String, errorPlaceHolderId: Int? = null): View {
|
||||
val api = JellyfinApi.getInstance(context.applicationContext)
|
||||
|
||||
return Glide
|
||||
.with(context)
|
||||
.load("${api.api.baseUrl}$url")
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.also { if (errorPlaceHolderId != null) error(errorPlaceHolderId) }
|
||||
.into(this)
|
||||
.view
|
||||
}
|
||||
|
||||
private fun View.posterDescription(name: String?) {
|
||||
contentDescription = String.format(context.resources.getString(R.string.image_description_poster), name)
|
||||
}
|
||||
|
||||
private fun View.backdropDescription(name: String?) {
|
||||
contentDescription = String.format(context.resources.getString(R.string.image_description_backdrop), name)
|
||||
}
|
|
@ -24,18 +24,13 @@ import java.util.UUID
|
|||
* @param baseUrl The url of the server
|
||||
* @constructor Creates a new [JellyfinApi] instance
|
||||
*/
|
||||
class JellyfinApi(androidContext: Context, baseUrl: String) {
|
||||
|
||||
class JellyfinApi(androidContext: Context) {
|
||||
val jellyfin = createJellyfin {
|
||||
clientInfo = ClientInfo(
|
||||
name = androidContext.applicationInfo.loadLabel(androidContext.packageManager)
|
||||
.toString(),
|
||||
version = BuildConfig.VERSION_NAME
|
||||
)
|
||||
clientInfo =
|
||||
ClientInfo(name = androidContext.applicationInfo.loadLabel(androidContext.packageManager).toString(), version = BuildConfig.VERSION_NAME)
|
||||
context = androidContext
|
||||
}
|
||||
|
||||
val api = jellyfin.createApi(baseUrl = baseUrl)
|
||||
val api = jellyfin.createApi()
|
||||
var userId: UUID? = null
|
||||
|
||||
val devicesApi = api.devicesApi
|
||||
|
@ -54,37 +49,15 @@ class JellyfinApi(androidContext: Context, baseUrl: String) {
|
|||
@Volatile
|
||||
private var INSTANCE: JellyfinApi? = null
|
||||
|
||||
/**
|
||||
* Creates or gets a new instance of [JellyfinApi]
|
||||
*
|
||||
* If there already is an instance, it will return that instance and ignore the [baseUrl] parameter
|
||||
*
|
||||
* @param context The context
|
||||
* @param baseUrl The url of the server
|
||||
*/
|
||||
fun getInstance(context: Context, baseUrl: String): JellyfinApi {
|
||||
fun getInstance(context: Context): JellyfinApi {
|
||||
synchronized(this) {
|
||||
var instance = INSTANCE
|
||||
if (instance == null) {
|
||||
instance = JellyfinApi(context.applicationContext, baseUrl)
|
||||
instance = JellyfinApi(context.applicationContext)
|
||||
INSTANCE = instance
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new [JellyfinApi] instance
|
||||
*
|
||||
* @param context The context
|
||||
* @param baseUrl The url of the server
|
||||
*/
|
||||
fun newInstance(context: Context, baseUrl: String): JellyfinApi {
|
||||
synchronized(this) {
|
||||
val instance = JellyfinApi(context.applicationContext, baseUrl)
|
||||
INSTANCE = instance
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
@ -22,7 +22,7 @@ object ApiModule {
|
|||
sharedPreferences: SharedPreferences,
|
||||
serverDatabase: ServerDatabaseDao
|
||||
): JellyfinApi {
|
||||
val jellyfinApi = JellyfinApi.getInstance(application, "")
|
||||
val jellyfinApi = JellyfinApi.getInstance(application)
|
||||
|
||||
val serverId = sharedPreferences.getString("selectedServer", null)
|
||||
if (serverId != null) {
|
||||
|
|
51
app/src/main/java/dev/jdtech/jellyfin/di/GlideModule.kt
Normal file
51
app/src/main/java/dev/jdtech/jellyfin/di/GlideModule.kt
Normal file
|
@ -0,0 +1,51 @@
|
|||
package dev.jdtech.jellyfin.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy.NONE
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy.RESOURCE
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
private const val cacheDefaultSize = 250
|
||||
|
||||
@GlideModule
|
||||
class GlideModule : AppGlideModule() {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val use = preferences.getBoolean("use_image_cache", false)
|
||||
|
||||
if (use) {
|
||||
val sizeMb = preferences.getString("image_cache_size", "$cacheDefaultSize")?.toInt()!!
|
||||
val sizeB = 1024L * 1024 * sizeMb
|
||||
Timber.d("Setting image cache to use $sizeMb MB of disk space")
|
||||
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, sizeB))
|
||||
builder.caching(enabled = true)
|
||||
} else {
|
||||
builder.caching(enabled = false)
|
||||
Timber.d("Image cache disabled. Clearing all persisted data.")
|
||||
|
||||
MainScope().launch {
|
||||
GlideApp.getPhotoCacheDir(context)?.also {
|
||||
if (it.exists()) it.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun GlideBuilder.caching(enabled: Boolean) {
|
||||
setDefaultRequestOptions(
|
||||
RequestOptions().diskCacheStrategy(
|
||||
if (enabled) RESOURCE else NONE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package dev.jdtech.jellyfin.fragments
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
|
||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
|
||||
|
@ -53,6 +54,10 @@ class SettingsFragment: PreferenceFragmentCompat() {
|
|||
true
|
||||
}
|
||||
|
||||
findPreference<EditTextPreference>("image_cache_size")?.setOnBindEditTextListener { editText ->
|
||||
editText.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
}
|
||||
|
||||
findPreference<EditTextPreference>("deviceName")?.setOnPreferenceChangeListener { _, name ->
|
||||
viewModel.updateDeviceName(name.toString())
|
||||
true
|
||||
|
|
|
@ -55,8 +55,12 @@
|
|||
<string name="settings_category_player">Přehrávač</string>
|
||||
<string name="manage_servers">Spravovat servery</string>
|
||||
<string name="settings_category_appearance">Vzhled</string>
|
||||
<string name="settings_category_device">Zařízení</string>
|
||||
<string name="device_name">Název zařízení</string>
|
||||
<string name="settings_category_device">Zařízení</string>
|
||||
<string name="settings_use_cache_title">Cachovat obrázky</string>
|
||||
<string name="settings_use_cache_summary">Cachování obrázků urychluje načítání obsahu. Projeví se po restartu aplikace.</string>
|
||||
<string name="settings_cache_size">Nastavit velikost cache v MB</string>
|
||||
<string name="settings_cache_size_message">Aplikace použije tuto velikost v MB k uložení obrázků z Jellyfin serveru. Větší hodnota může být prospěšná na pomalejším připojení.</string>
|
||||
<string name="theme">Téma</string>
|
||||
<string name="error_preparing_player_items">Chyba při načítání přehrávání.</string>
|
||||
<string name="view_details">Zobrazit detaily</string>
|
||||
|
@ -71,4 +75,6 @@
|
|||
<string name="mpv_player_summary">Použít experimentální MPV přehrávač k přehrávání videí. MPV má podporu pro více video a audio kodeků a formátů titulků.</string>
|
||||
<string name="force_software_decoding">Vynutit softwarové dekódování</string>
|
||||
<string name="force_software_decoding_summary">Vypnout hardwarové dekódování a vynutit softwarové dekódování. Může být užitečné pokud hardwarové dekódování produkuje artefakty.</string>
|
||||
<string name="image_description_poster">%1$s plakát</string>
|
||||
<string name="image_description_backdrop">%1$s pozadí</string>
|
||||
</resources>
|
|
@ -59,8 +59,13 @@
|
|||
<string name="settings_category_player">Player</string>
|
||||
<string name="manage_servers">Manage servers</string>
|
||||
<string name="settings_category_appearance">Appearance</string>
|
||||
<string name="settings_category_device">Device</string>
|
||||
<string name="device_name">Device name</string>
|
||||
<string name="settings_category_device">Device</string>
|
||||
<string name="settings_category_cache">Cache</string>
|
||||
<string name="settings_use_cache_title">Cache images</string>
|
||||
<string name="settings_use_cache_summary">Cache images on disk to speed up loading times. Will take effect after app restart.</string>
|
||||
<string name="settings_cache_size">Cache size (MB)</string>
|
||||
<string name="settings_cache_size_message">App will use this amount of MB of your disk space to store images from Jellyfin server. Larger values might be beneficial on slower networks.</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="error_preparing_player_items">Error preparing player items.</string>
|
||||
<string name="view_details">View details</string>
|
||||
|
@ -89,6 +94,9 @@
|
|||
<string name="sort_order">Sort order</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="image_description_poster">%1$s poster</string>
|
||||
<string name="image_description_backdrop">%1$s backdrop</string>
|
||||
|
||||
<string-array name="sort_by_options">
|
||||
<item>Name</item>
|
||||
<item>IMDB Rating</item>
|
||||
|
|
|
@ -7,28 +7,28 @@
|
|||
app:defaultValue="null"
|
||||
app:entries="@array/languages"
|
||||
app:entryValues="@array/languages_values"
|
||||
app:icon="@drawable/ic_speaker"
|
||||
app:key="audio_language"
|
||||
app:title="@string/settings_preferred_audio_language"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:icon="@drawable/ic_speaker"/>
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="null"
|
||||
app:entries="@array/languages"
|
||||
app:entryValues="@array/languages_values"
|
||||
app:icon="@drawable/ic_closed_caption"
|
||||
app:key="subtitle_language"
|
||||
app:title="@string/settings_preferred_subtitle_language"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:icon="@drawable/ic_closed_caption"/>
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/settings_category_servers">
|
||||
|
||||
<Preference
|
||||
app:icon="@drawable/ic_server"
|
||||
app:key="switchServer"
|
||||
app:title="@string/manage_servers"
|
||||
app:icon="@drawable/ic_server"/>
|
||||
app:title="@string/manage_servers" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
@ -37,29 +37,43 @@
|
|||
app:defaultValue="system"
|
||||
app:entries="@array/themes"
|
||||
app:entryValues="@array/themes_value"
|
||||
app:icon="@drawable/ic_palette"
|
||||
app:key="theme"
|
||||
app:title="@string/theme"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:icon="@drawable/ic_palette"/>
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/settings_category_player">
|
||||
<SwitchPreference
|
||||
app:key="mpv_player"
|
||||
app:title="@string/mpv_player"
|
||||
app:summary="@string/mpv_player_summary"/>
|
||||
app:summary="@string/mpv_player_summary"
|
||||
app:title="@string/mpv_player" />
|
||||
<SwitchPreference
|
||||
app:key="mpv_disable_hwdec"
|
||||
app:dependency="mpv_player"
|
||||
app:title="@string/force_software_decoding"
|
||||
app:summary="@string/force_software_decoding_summary"/>
|
||||
app:key="mpv_disable_hwdec"
|
||||
app:summary="@string/force_software_decoding_summary"
|
||||
app:title="@string/force_software_decoding" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/settings_category_device">
|
||||
<EditTextPreference
|
||||
app:key="deviceName"
|
||||
app:title="@string/device_name"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/settings_category_cache">
|
||||
<SwitchPreference
|
||||
app:key="use_image_cache"
|
||||
app:summary="@string/settings_use_cache_summary"
|
||||
app:title="@string/settings_use_cache_title" />
|
||||
<EditTextPreference
|
||||
app:defaultValue="250"
|
||||
app:dependency="use_image_cache"
|
||||
app:dialogMessage="@string/settings_cache_size_message"
|
||||
app:key="image_cache_size"
|
||||
app:title="@string/settings_cache_size"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/about">
|
||||
|
|
Loading…
Reference in a new issue