Android: Replace Glide with Coil image loading
This commit is contained in:
parent
e6583f8bec
commit
28faca63a6
|
@ -155,7 +155,7 @@ dependencies {
|
||||||
implementation 'com.android.volley:volley:1.2.1'
|
implementation 'com.android.volley:volley:1.2.1'
|
||||||
|
|
||||||
// For loading game covers from disk and GameTDB
|
// For loading game covers from disk and GameTDB
|
||||||
implementation 'com.github.bumptech.glide:glide:4.13.2'
|
implementation 'io.coil-kt:coil:2.2.2'
|
||||||
|
|
||||||
implementation 'com.nononsenseapps:filepicker:4.2.1'
|
implementation 'com.nononsenseapps:filepicker:4.2.1'
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
package org.dolphinemu.dolphinemu.adapters
|
package org.dolphinemu.dolphinemu.adapters
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder
|
import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder
|
||||||
import android.view.View.OnLongClickListener
|
import android.view.View.OnLongClickListener
|
||||||
|
@ -11,29 +10,23 @@ import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import org.dolphinemu.dolphinemu.utils.GlideUtils
|
|
||||||
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||||
import org.dolphinemu.dolphinemu.R
|
import org.dolphinemu.dolphinemu.R
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import org.dolphinemu.dolphinemu.activities.EmulationActivity
|
import org.dolphinemu.dolphinemu.activities.EmulationActivity
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.dolphinemu.dolphinemu.databinding.CardGameBinding
|
import org.dolphinemu.dolphinemu.databinding.CardGameBinding
|
||||||
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog
|
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||||
|
import org.dolphinemu.dolphinemu.utils.CoilUtils
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
|
|
||||||
class GameAdapter(activity: Activity) : RecyclerView.Adapter<GameViewHolder>(),
|
class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapter<GameViewHolder>(),
|
||||||
View.OnClickListener, OnLongClickListener {
|
View.OnClickListener, OnLongClickListener {
|
||||||
private var mGameFiles: List<GameFile>
|
private var mGameFiles: List<GameFile> = ArrayList()
|
||||||
private val mActivity: Activity
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
|
|
||||||
* display no data until swapDataSet is called.
|
|
||||||
*/
|
|
||||||
init {
|
|
||||||
mGameFiles = ArrayList()
|
|
||||||
mActivity = activity
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by the LayoutManager when it is necessary to create a new view.
|
* Called by the LayoutManager when it is necessary to create a new view.
|
||||||
|
@ -65,7 +58,33 @@ class GameAdapter(activity: Activity) : RecyclerView.Adapter<GameViewHolder>(),
|
||||||
val context = holder.itemView.context
|
val context = holder.itemView.context
|
||||||
val gameFile = mGameFiles[position]
|
val gameFile = mGameFiles[position]
|
||||||
|
|
||||||
GlideUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile, mActivity)
|
holder.apply {
|
||||||
|
if (BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) {
|
||||||
|
binding.textGameTitle.text = gameFile.title
|
||||||
|
binding.textGameTitle.visibility = View.VISIBLE
|
||||||
|
binding.textGameTitleInner.visibility = View.GONE
|
||||||
|
binding.textGameCaption.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.textGameTitleInner.text = gameFile.title
|
||||||
|
binding.textGameTitleInner.visibility = View.VISIBLE
|
||||||
|
binding.textGameTitle.visibility = View.GONE
|
||||||
|
binding.textGameCaption.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mActivity.lifecycleScope.launchWhenStarted {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val customCoverUri = CoilUtils.findCustomCover(gameFile)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
CoilUtils.loadGameCover(
|
||||||
|
holder,
|
||||||
|
holder.binding.imageGameScreen,
|
||||||
|
gameFile,
|
||||||
|
customCoverUri
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in)
|
val animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in)
|
||||||
animateIn.fillAfter = true
|
animateIn.fillAfter = true
|
||||||
|
@ -86,15 +105,11 @@ class GameAdapter(activity: Activity) : RecyclerView.Adapter<GameViewHolder>(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GameViewHolder(binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) {
|
class GameViewHolder(var binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
var gameFile: GameFile? = null
|
var gameFile: GameFile? = null
|
||||||
|
|
||||||
@JvmField
|
|
||||||
var binding: CardGameBinding
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.root.tag = this
|
binding.root.tag = this
|
||||||
this.binding = binding
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,20 +7,24 @@ import android.view.ViewGroup
|
||||||
import androidx.leanback.widget.ImageCardView
|
import androidx.leanback.widget.ImageCardView
|
||||||
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder
|
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
import org.dolphinemu.dolphinemu.utils.GlideUtils
|
|
||||||
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||||
import org.dolphinemu.dolphinemu.R
|
import org.dolphinemu.dolphinemu.R
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog
|
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog
|
||||||
|
import org.dolphinemu.dolphinemu.utils.CoilUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Leanback library / docs call this a Presenter, but it works very
|
* The Leanback library / docs call this a Presenter, but it works very
|
||||||
* similarly to a RecyclerView.Adapter.
|
* similarly to a RecyclerView.Adapter.
|
||||||
*/
|
*/
|
||||||
class GameRowPresenter : Presenter() {
|
class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
||||||
// Create a new view.
|
// Create a new view.
|
||||||
val gameCard = ImageCardView(parent.context)
|
val gameCard = ImageCardView(parent.context)
|
||||||
|
@ -63,7 +67,20 @@ class GameRowPresenter : Presenter() {
|
||||||
holder.cardParent.contentText = gameFile.company
|
holder.cardParent.contentText = gameFile.company
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GlideUtils.loadGameCover(null, holder.imageScreenshot, gameFile, null)
|
|
||||||
|
mActivity.lifecycleScope.launchWhenStarted {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val customCoverUri = CoilUtils.findCustomCover(gameFile)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
CoilUtils.loadGameCover(
|
||||||
|
null,
|
||||||
|
holder.imageScreenshot,
|
||||||
|
gameFile,
|
||||||
|
customCoverUri
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
|
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
|
||||||
|
|
|
@ -3,17 +3,23 @@
|
||||||
package org.dolphinemu.dolphinemu.dialogs
|
package org.dolphinemu.dolphinemu.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||||
import org.dolphinemu.dolphinemu.R
|
import org.dolphinemu.dolphinemu.R
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.dolphinemu.dolphinemu.NativeLibrary
|
import org.dolphinemu.dolphinemu.NativeLibrary
|
||||||
import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsBinding
|
import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsBinding
|
||||||
import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsTvBinding
|
import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsTvBinding
|
||||||
import org.dolphinemu.dolphinemu.utils.GlideUtils
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
|
|
||||||
class GameDetailsDialog : DialogFragment() {
|
class GameDetailsDialog : DialogFragment() {
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
@ -73,7 +79,9 @@ class GameDetailsDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GlideUtils.loadGameBanner(binding.banner, gameFile)
|
this.lifecycleScope.launch {
|
||||||
|
loadGameBanner(binding.banner, gameFile)
|
||||||
|
}
|
||||||
|
|
||||||
builder.setView(binding.root)
|
builder.setView(binding.root)
|
||||||
} else {
|
} else {
|
||||||
|
@ -123,13 +131,35 @@ class GameDetailsDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GlideUtils.loadGameBanner(tvBinding.banner, gameFile)
|
this.lifecycleScope.launch {
|
||||||
|
loadGameBanner(tvBinding.banner, gameFile)
|
||||||
|
}
|
||||||
|
|
||||||
builder.setView(tvBinding.root)
|
builder.setView(tvBinding.root)
|
||||||
}
|
}
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadGameBanner(imageView: ImageView, gameFile: GameFile) {
|
||||||
|
val vector = gameFile.banner
|
||||||
|
val width = gameFile.bannerWidth
|
||||||
|
val height = gameFile.bannerHeight
|
||||||
|
|
||||||
|
imageView.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||||
|
val request = ImageRequest.Builder(imageView.context)
|
||||||
|
.target(imageView)
|
||||||
|
.error(R.drawable.no_banner)
|
||||||
|
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
bitmap.setPixels(vector, 0, width, 0, 0, width, height)
|
||||||
|
request.data(bitmap)
|
||||||
|
} else {
|
||||||
|
request.data(R.drawable.no_banner)
|
||||||
|
}
|
||||||
|
imageView.context.imageLoader.execute(request.build())
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_GAME_PATH = "game_path"
|
private const val ARG_GAME_PATH = "game_path"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.dolphinemu.dolphinemu.fragments
|
package org.dolphinemu.dolphinemu.fragments
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -88,7 +89,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
|
||||||
NativeConfig.LAYER_BASE,
|
NativeConfig.LAYER_BASE,
|
||||||
mBindingMobile.switchDownloadCovers.isChecked
|
mBindingMobile.switchDownloadCovers.isChecked
|
||||||
)
|
)
|
||||||
mView.reloadGrid()
|
(mView as Activity).recreate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +119,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
|
||||||
NativeConfig.LAYER_BASE,
|
NativeConfig.LAYER_BASE,
|
||||||
mBindingTv.switchDownloadCovers.isChecked
|
mBindingTv.switchDownloadCovers.isChecked
|
||||||
)
|
)
|
||||||
mView.reloadGrid()
|
(mView as Activity).recreate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.model;
|
package org.dolphinemu.dolphinemu.model;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
import androidx.annotation.Keep;
|
||||||
|
|
||||||
public class GameFile
|
public class GameFile
|
||||||
{
|
{
|
||||||
|
public static int REGION_NTSC_J = 0;
|
||||||
|
public static int REGION_NTSC_U = 1;
|
||||||
|
public static int REGION_PAL = 2;
|
||||||
|
public static int REGION_NTSC_K = 4;
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
private long mPointer;
|
private long mPointer;
|
||||||
|
|
||||||
|
@ -68,11 +71,6 @@ public class GameFile
|
||||||
|
|
||||||
public native int getBannerHeight();
|
public native int getBannerHeight();
|
||||||
|
|
||||||
public String getCoverPath(Context context)
|
|
||||||
{
|
|
||||||
return context.getExternalCacheDir().getPath() + "/GameCovers/" + getGameTdbId() + ".png";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCustomCoverPath()
|
public String getCustomCoverPath()
|
||||||
{
|
{
|
||||||
return getPath().substring(0, getPath().lastIndexOf(".")) + ".cover.png";
|
return getPath().substring(0, getPath().lastIndexOf(".")) + ".cover.png";
|
||||||
|
|
|
@ -349,7 +349,7 @@ public final class TvMainActivity extends FragmentActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an adapter for this row.
|
// Create an adapter for this row.
|
||||||
ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter());
|
ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter(this));
|
||||||
row.addAll(0, gameFiles);
|
row.addAll(0, gameFiles);
|
||||||
|
|
||||||
// Keep a reference to the row in case we need to refresh it.
|
// Keep a reference to the row in case we need to refresh it.
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.dolphinemu.dolphinemu.utils
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import coil.load
|
||||||
|
import coil.target.ImageViewTarget
|
||||||
|
import org.dolphinemu.dolphinemu.R
|
||||||
|
import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
object CoilUtils {
|
||||||
|
fun loadGameCover(
|
||||||
|
gameViewHolder: GameViewHolder?,
|
||||||
|
imageView: ImageView,
|
||||||
|
gameFile: GameFile,
|
||||||
|
customCoverUri: Uri?
|
||||||
|
) {
|
||||||
|
imageView.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||||
|
val imageTarget = ImageViewTarget(imageView)
|
||||||
|
if (customCoverUri != null) {
|
||||||
|
imageView.load(customCoverUri) {
|
||||||
|
error(R.drawable.no_banner)
|
||||||
|
target(
|
||||||
|
onSuccess = { success ->
|
||||||
|
disableInnerTitle(gameViewHolder)
|
||||||
|
imageTarget.drawable = success
|
||||||
|
},
|
||||||
|
onError = { error ->
|
||||||
|
enableInnerTitle(gameViewHolder)
|
||||||
|
imageTarget.drawable = error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (BooleanSetting.MAIN_USE_GAME_COVERS.booleanGlobal) {
|
||||||
|
imageView.load(CoverHelper.buildGameTDBUrl(gameFile, CoverHelper.getRegion(gameFile))) {
|
||||||
|
error(R.drawable.no_banner)
|
||||||
|
target(
|
||||||
|
onSuccess = { success ->
|
||||||
|
disableInnerTitle(gameViewHolder)
|
||||||
|
imageTarget.drawable = success
|
||||||
|
},
|
||||||
|
onError = { error ->
|
||||||
|
enableInnerTitle(gameViewHolder)
|
||||||
|
imageTarget.drawable = error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imageView.load(R.drawable.no_banner)
|
||||||
|
enableInnerTitle(gameViewHolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableInnerTitle(gameViewHolder: GameViewHolder?) {
|
||||||
|
if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) {
|
||||||
|
gameViewHolder.binding.textGameTitleInner.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableInnerTitle(gameViewHolder: GameViewHolder?) {
|
||||||
|
if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) {
|
||||||
|
gameViewHolder.binding.textGameTitleInner.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findCustomCover(gameFile: GameFile): Uri? {
|
||||||
|
val customCoverPath = gameFile.customCoverPath
|
||||||
|
var customCoverUri: Uri? = null
|
||||||
|
var customCoverExists = false
|
||||||
|
if (ContentHandler.isContentUri(customCoverPath)) {
|
||||||
|
try {
|
||||||
|
customCoverUri = ContentHandler.unmangle(customCoverPath)
|
||||||
|
customCoverExists = true
|
||||||
|
} catch (ignored: FileNotFoundException) {
|
||||||
|
} catch (ignored: SecurityException) {
|
||||||
|
// Let customCoverExists remain false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customCoverUri = Uri.parse(customCoverPath)
|
||||||
|
customCoverExists = File(customCoverPath).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (customCoverExists) {
|
||||||
|
customCoverUri
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,44 +3,34 @@
|
||||||
package org.dolphinemu.dolphinemu.utils
|
package org.dolphinemu.dolphinemu.utils
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
import android.graphics.Bitmap
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
object CoverHelper {
|
object CoverHelper {
|
||||||
|
@JvmStatic
|
||||||
fun buildGameTDBUrl(game: GameFile, region: String?): String {
|
fun buildGameTDBUrl(game: GameFile, region: String?): String {
|
||||||
val baseUrl = "https://art.gametdb.com/wii/cover/%s/%s.png"
|
val baseUrl = "https://art.gametdb.com/wii/cover/%s/%s.png"
|
||||||
return String.format(baseUrl, region, game.gameTdbId)
|
return String.format(baseUrl, region, game.gameTdbId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getRegion(game: GameFile): String {
|
fun getRegion(game: GameFile): String {
|
||||||
val region: String = when (game.region) {
|
val region: String = when (game.region) {
|
||||||
0 -> "JA"
|
GameFile.REGION_NTSC_J -> "JA"
|
||||||
1 -> "US"
|
GameFile.REGION_NTSC_U -> "US"
|
||||||
4 -> "KO"
|
GameFile.REGION_NTSC_K -> "KO"
|
||||||
2 -> when (game.country) {
|
GameFile.REGION_PAL -> when (game.country) {
|
||||||
3 -> "AU"
|
3 -> "AU" // Australia
|
||||||
4 -> "FR"
|
4 -> "FR" // France
|
||||||
5 -> "DE"
|
5 -> "DE" // Germany
|
||||||
6 -> "IT"
|
6 -> "IT" // Italy
|
||||||
8 -> "NL"
|
8 -> "NL" // Netherlands
|
||||||
9 -> "RU"
|
9 -> "RU" // Russia
|
||||||
10 -> "ES"
|
10 -> "ES" // Spain
|
||||||
0 -> "EN"
|
0 -> "EN" // Europe
|
||||||
else -> "EN"
|
else -> "EN"
|
||||||
}
|
}
|
||||||
3 -> "EN"
|
3 -> "EN" // Unknown
|
||||||
else -> "EN"
|
else -> "EN"
|
||||||
}
|
}
|
||||||
return region
|
return region
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCover(cover: Bitmap, path: String?) {
|
|
||||||
try {
|
|
||||||
val out = FileOutputStream(path)
|
|
||||||
cover.compress(Bitmap.CompressFormat.PNG, 100, out)
|
|
||||||
out.close()
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,233 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.utils
|
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.utils.CoverHelper.buildGameTDBUrl
|
|
||||||
import org.dolphinemu.dolphinemu.utils.CoverHelper.getRegion
|
|
||||||
import org.dolphinemu.dolphinemu.utils.CoverHelper.saveCover
|
|
||||||
import android.os.Looper
|
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
||||||
import org.dolphinemu.dolphinemu.R
|
|
||||||
import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder
|
|
||||||
import android.app.Activity
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.drawable.BitmapDrawable
|
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
|
||||||
import com.bumptech.glide.request.RequestListener
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Handler
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageView
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
|
||||||
import com.bumptech.glide.request.target.Target
|
|
||||||
import com.bumptech.glide.request.transition.Transition
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
object GlideUtils {
|
|
||||||
private val saveCoverExecutor = Executors.newSingleThreadExecutor()
|
|
||||||
private val unmangleExecutor = Executors.newSingleThreadExecutor()
|
|
||||||
private val unmangleHandler = Handler(Looper.getMainLooper())
|
|
||||||
|
|
||||||
fun loadGameBanner(imageView: ImageView, gameFile: GameFile) {
|
|
||||||
val context = imageView.context
|
|
||||||
val vector = gameFile.banner
|
|
||||||
val width = gameFile.bannerWidth
|
|
||||||
val height = gameFile.bannerHeight
|
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
||||||
bitmap.setPixels(vector, 0, width, 0, 0, width, height)
|
|
||||||
Glide.with(context)
|
|
||||||
.load(bitmap)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.centerCrop()
|
|
||||||
.into(imageView)
|
|
||||||
} else {
|
|
||||||
Glide.with(context)
|
|
||||||
.load(R.drawable.no_banner)
|
|
||||||
.into(imageView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadGameCover(
|
|
||||||
gameViewHolder: GameViewHolder?,
|
|
||||||
imageView: ImageView,
|
|
||||||
gameFile: GameFile,
|
|
||||||
activity: Activity?
|
|
||||||
) {
|
|
||||||
gameViewHolder?.apply {
|
|
||||||
if (BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) {
|
|
||||||
binding.textGameTitle.text = gameFile.title
|
|
||||||
binding.textGameTitle.visibility = View.VISIBLE
|
|
||||||
binding.textGameTitleInner.visibility = View.GONE
|
|
||||||
binding.textGameCaption.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.textGameTitleInner.text = gameFile.title
|
|
||||||
binding.textGameTitle.visibility = View.GONE
|
|
||||||
binding.textGameCaption.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unmangleExecutor.execute {
|
|
||||||
val customCoverPath = gameFile.customCoverPath
|
|
||||||
var customCoverUri: Uri? = null
|
|
||||||
var customCoverExists = false
|
|
||||||
if (ContentHandler.isContentUri(customCoverPath)) {
|
|
||||||
try {
|
|
||||||
customCoverUri = ContentHandler.unmangle(customCoverPath)
|
|
||||||
customCoverExists = true
|
|
||||||
} catch (ignored: FileNotFoundException) {
|
|
||||||
// Let customCoverExists remain false
|
|
||||||
} catch (ignored: SecurityException) {
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
customCoverUri = Uri.parse(customCoverPath)
|
|
||||||
customCoverExists = File(customCoverPath).exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = imageView.context
|
|
||||||
val finalCustomCoverExists = customCoverExists
|
|
||||||
val finalCustomCoverUri = customCoverUri
|
|
||||||
|
|
||||||
val cover = File(gameFile.getCoverPath(context))
|
|
||||||
val cachedCoverExists = cover.exists()
|
|
||||||
unmangleHandler.post {
|
|
||||||
// We can't get a reference to the current activity in the TV version.
|
|
||||||
// Luckily it won't attempt to start loads on destroyed activities.
|
|
||||||
if (activity != null) {
|
|
||||||
// We can't start an image load on a destroyed activity
|
|
||||||
if (activity.isDestroyed) {
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalCustomCoverExists) {
|
|
||||||
Glide.with(imageView)
|
|
||||||
.load(finalCustomCoverUri)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.centerCrop()
|
|
||||||
.error(R.drawable.no_banner)
|
|
||||||
.listener(object : RequestListener<Drawable?> {
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any,
|
|
||||||
target: Target<Drawable?>,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
enableInnerTitle(gameViewHolder)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: Drawable?,
|
|
||||||
model: Any,
|
|
||||||
target: Target<Drawable?>,
|
|
||||||
dataSource: DataSource,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
disableInnerTitle(gameViewHolder)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(imageView)
|
|
||||||
} else if (cachedCoverExists) {
|
|
||||||
Glide.with(imageView)
|
|
||||||
.load(cover)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.centerCrop()
|
|
||||||
.error(R.drawable.no_banner)
|
|
||||||
.listener(object : RequestListener<Drawable?> {
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any,
|
|
||||||
target: Target<Drawable?>,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
enableInnerTitle(gameViewHolder)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: Drawable?,
|
|
||||||
model: Any,
|
|
||||||
target: Target<Drawable?>,
|
|
||||||
dataSource: DataSource,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
disableInnerTitle(gameViewHolder)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(imageView)
|
|
||||||
} else if (BooleanSetting.MAIN_USE_GAME_COVERS.booleanGlobal) {
|
|
||||||
Glide.with(context)
|
|
||||||
.load(buildGameTDBUrl(gameFile, getRegion(gameFile)))
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.centerCrop()
|
|
||||||
.error(R.drawable.no_banner)
|
|
||||||
.listener(object : RequestListener<Drawable?> {
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any,
|
|
||||||
target: Target<Drawable?>,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
enableInnerTitle(gameViewHolder)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: Drawable?,
|
|
||||||
model: Any,
|
|
||||||
target: Target<Drawable?>,
|
|
||||||
dataSource: DataSource,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
disableInnerTitle(gameViewHolder)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(object : CustomTarget<Drawable?>() {
|
|
||||||
override fun onLoadCleared(placeholder: Drawable?) {}
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: Drawable,
|
|
||||||
transition: Transition<in Drawable?>?
|
|
||||||
) {
|
|
||||||
val savedCover = (resource as BitmapDrawable).bitmap
|
|
||||||
saveCoverExecutor.execute {
|
|
||||||
saveCover(
|
|
||||||
savedCover,
|
|
||||||
gameFile.getCoverPath(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
imageView.setImageBitmap(savedCover)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Glide.with(imageView.context)
|
|
||||||
.load(R.drawable.no_banner)
|
|
||||||
.into(imageView)
|
|
||||||
enableInnerTitle(gameViewHolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableInnerTitle(gameViewHolder: GameViewHolder?) {
|
|
||||||
if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) {
|
|
||||||
gameViewHolder.binding.textGameTitleInner.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun disableInnerTitle(gameViewHolder: GameViewHolder?) {
|
|
||||||
if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) {
|
|
||||||
gameViewHolder.binding.textGameTitleInner.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -186,9 +186,9 @@ public class TvUtil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentUri == null && (cover = new File(game.getCoverPath(context))).exists())
|
if (contentUri == null)
|
||||||
{
|
{
|
||||||
contentUri = getUriForFile(context, getFileProvider(context), cover);
|
contentUri = Uri.parse(CoverHelper.buildGameTDBUrl(game, CoverHelper.getRegion(game)));
|
||||||
}
|
}
|
||||||
|
|
||||||
context.grantUriPermission(LEANBACK_PACKAGE, contentUri, FLAG_GRANT_READ_URI_PERMISSION);
|
context.grantUriPermission(LEANBACK_PACKAGE, contentUri, FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
Loading…
Reference in New Issue