diff --git a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt index 3604d0ca..b2036f04 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt @@ -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?) { @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?) { @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?) { 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 fun bindDownloadSections(recyclerView: RecyclerView, data: List?) { 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) } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt b/app/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt index 568e5a03..c0086fad 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt @@ -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 - } - } } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt b/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt index b86b45b7..8be4797c 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/di/ApiModule.kt @@ -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) { diff --git a/app/src/main/java/dev/jdtech/jellyfin/di/GlideModule.kt b/app/src/main/java/dev/jdtech/jellyfin/di/GlideModule.kt new file mode 100644 index 00000000..05dc9ad0 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/di/GlideModule.kt @@ -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 + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt index 88518220..d6885aad 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt @@ -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("image_cache_size")?.setOnBindEditTextListener { editText -> + editText.inputType = InputType.TYPE_CLASS_NUMBER + } + findPreference("deviceName")?.setOnPreferenceChangeListener { _, name -> viewModel.updateDeviceName(name.toString()) true diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 40f3f773..c13db457 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -55,8 +55,12 @@ Přehrávač Spravovat servery Vzhled - Zařízení Název zařízení + Zařízení + Cachovat obrázky + Cachování obrázků urychluje načítání obsahu. Projeví se po restartu aplikace. + Nastavit velikost cache v MB + 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í. Téma Chyba při načítání přehrávání. Zobrazit detaily @@ -71,4 +75,6 @@ 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ů. Vynutit softwarové dekódování 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. + %1$s plakát + %1$s pozadí \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 248f6c66..87b86feb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,8 +59,13 @@ Player Manage servers Appearance - Device Device name + Device + Cache + Cache images + Cache images on disk to speed up loading times. Will take effect after app restart. + Cache size (MB) + 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. Theme Error preparing player items. View details @@ -89,6 +94,9 @@ Sort order Close Share + %1$s poster + %1$s backdrop + Name IMDB Rating diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml index 5b53e488..60a313b1 100644 --- a/app/src/main/res/xml/fragment_settings.xml +++ b/app/src/main/res/xml/fragment_settings.xml @@ -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" /> + app:useSimpleSummaryProvider="true" /> + app:title="@string/manage_servers" /> @@ -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" /> + app:summary="@string/mpv_player_summary" + app:title="@string/mpv_player" /> + app:key="mpv_disable_hwdec" + app:summary="@string/force_software_decoding_summary" + app:title="@string/force_software_decoding" /> + app:useSimpleSummaryProvider="true" /> + + + + +