mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-24 14:49:42 -06:00
Merge pull request #11408 from t895/coil
Android: Rewrite image loading with Kotlin and Coil
This commit is contained in:
@ -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'
|
||||||
}
|
}
|
||||||
|
@ -1,184 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.adapters;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.AnimationUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.R;
|
|
||||||
import org.dolphinemu.dolphinemu.activities.EmulationActivity;
|
|
||||||
import org.dolphinemu.dolphinemu.databinding.CardGameBinding;
|
|
||||||
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog;
|
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile;
|
|
||||||
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
|
|
||||||
import org.dolphinemu.dolphinemu.utils.GlideUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class GameAdapter extends RecyclerView.Adapter<GameAdapter.GameViewHolder> implements
|
|
||||||
View.OnClickListener,
|
|
||||||
View.OnLongClickListener
|
|
||||||
{
|
|
||||||
private List<GameFile> mGameFiles;
|
|
||||||
private Activity mActivity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
|
|
||||||
* display no data until swapDataSet is called.
|
|
||||||
*/
|
|
||||||
public GameAdapter(Activity activity)
|
|
||||||
{
|
|
||||||
mGameFiles = new ArrayList<>();
|
|
||||||
mActivity = activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the LayoutManager when it is necessary to create a new view.
|
|
||||||
*
|
|
||||||
* @param parent The RecyclerView (I think?) the created view will be thrown into.
|
|
||||||
* @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
|
|
||||||
* @return The created ViewHolder with references to all the child view's members.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
|
|
||||||
{
|
|
||||||
CardGameBinding binding = CardGameBinding.inflate(LayoutInflater.from(parent.getContext()));
|
|
||||||
|
|
||||||
binding.getRoot().setOnClickListener(this);
|
|
||||||
binding.getRoot().setOnLongClickListener(this);
|
|
||||||
|
|
||||||
// Use that view to create a ViewHolder.
|
|
||||||
return new GameViewHolder(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the LayoutManager when a new view is not necessary because we can recycle
|
|
||||||
* an existing one (for example, if a view just scrolled onto the screen from the bottom, we
|
|
||||||
* can use the view that just scrolled off the top instead of inflating a new one.)
|
|
||||||
*
|
|
||||||
* @param holder A ViewHolder representing the view we're recycling.
|
|
||||||
* @param position The position of the 'new' view in the dataset.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(GameViewHolder holder, int position)
|
|
||||||
{
|
|
||||||
Context context = holder.itemView.getContext();
|
|
||||||
GameFile gameFile = mGameFiles.get(position);
|
|
||||||
GlideUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile, mActivity);
|
|
||||||
|
|
||||||
if (GameFileCacheManager.findSecondDisc(gameFile) != null)
|
|
||||||
{
|
|
||||||
holder.binding.textGameCaption
|
|
||||||
.setText(context.getString(R.string.disc_number, gameFile.getDiscNumber() + 1));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
holder.binding.textGameCaption.setText(gameFile.getCompany());
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.gameFile = gameFile;
|
|
||||||
|
|
||||||
Animation animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in);
|
|
||||||
animateIn.setFillAfter(true);
|
|
||||||
Animation animateOut = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_out);
|
|
||||||
animateOut.setFillAfter(true);
|
|
||||||
holder.binding.getRoot().setOnFocusChangeListener((v, hasFocus) ->
|
|
||||||
holder.binding.cardGameArt.startAnimation(hasFocus ? animateIn : animateOut));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GameViewHolder extends RecyclerView.ViewHolder
|
|
||||||
{
|
|
||||||
public GameFile gameFile;
|
|
||||||
public CardGameBinding binding;
|
|
||||||
|
|
||||||
public GameViewHolder(@NonNull CardGameBinding binding)
|
|
||||||
{
|
|
||||||
super(binding.getRoot());
|
|
||||||
binding.getRoot().setTag(this);
|
|
||||||
this.binding = binding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the LayoutManager to find out how much data we have.
|
|
||||||
*
|
|
||||||
* @return Size of the dataset.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int getItemCount()
|
|
||||||
{
|
|
||||||
return mGameFiles.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tell Android whether or not each item in the dataset has a stable identifier.
|
|
||||||
*
|
|
||||||
* @param hasStableIds ignored.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void setHasStableIds(boolean hasStableIds)
|
|
||||||
{
|
|
||||||
super.setHasStableIds(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a load is finished, call this to replace the existing data
|
|
||||||
* with the newly-loaded data.
|
|
||||||
*/
|
|
||||||
public void swapDataSet(List<GameFile> gameFiles)
|
|
||||||
{
|
|
||||||
mGameFiles = gameFiles;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-fetches game metadata from the game file cache.
|
|
||||||
*/
|
|
||||||
public void refetchMetadata()
|
|
||||||
{
|
|
||||||
notifyItemRangeChanged(0, getItemCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches the game that was clicked on.
|
|
||||||
*
|
|
||||||
* @param view The card representing the game the user wants to play.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onClick(View view)
|
|
||||||
{
|
|
||||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
|
||||||
|
|
||||||
String[] paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile);
|
|
||||||
EmulationActivity.launch((FragmentActivity) view.getContext(), paths, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches the details activity for this Game, using an ID stored in the
|
|
||||||
* details button's Tag.
|
|
||||||
*
|
|
||||||
* @param view The Card button that was long-clicked.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onLongClick(View view)
|
|
||||||
{
|
|
||||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
|
||||||
|
|
||||||
GamePropertiesDialog fragment = GamePropertiesDialog.newInstance(holder.gameFile);
|
|
||||||
((FragmentActivity) view.getContext()).getSupportFragmentManager().beginTransaction()
|
|
||||||
.add(fragment, GamePropertiesDialog.TAG).commit();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,175 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.dolphinemu.dolphinemu.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder
|
||||||
|
import android.view.View.OnLongClickListener
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||||
|
import org.dolphinemu.dolphinemu.R
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import org.dolphinemu.dolphinemu.activities.EmulationActivity
|
||||||
|
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.dialogs.GamePropertiesDialog
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||||
|
import org.dolphinemu.dolphinemu.utils.CoilUtils
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapter<GameViewHolder>(),
|
||||||
|
View.OnClickListener, OnLongClickListener {
|
||||||
|
private var mGameFiles: List<GameFile> = ArrayList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the LayoutManager when it is necessary to create a new view.
|
||||||
|
*
|
||||||
|
* @param parent The RecyclerView (I think?) the created view will be thrown into.
|
||||||
|
* @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
|
||||||
|
* @return The created ViewHolder with references to all the child view's members.
|
||||||
|
*/
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||||
|
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context))
|
||||||
|
binding.root.apply {
|
||||||
|
setOnClickListener(this@GameAdapter)
|
||||||
|
setOnLongClickListener(this@GameAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use that view to create a ViewHolder.
|
||||||
|
return GameViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the LayoutManager when a new view is not necessary because we can recycle
|
||||||
|
* an existing one (for example, if a view just scrolled onto the screen from the bottom, we
|
||||||
|
* can use the view that just scrolled off the top instead of inflating a new one.)
|
||||||
|
*
|
||||||
|
* @param holder A ViewHolder representing the view we're recycling.
|
||||||
|
* @param position The position of the 'new' view in the dataset.
|
||||||
|
*/
|
||||||
|
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
|
||||||
|
val context = holder.itemView.context
|
||||||
|
val gameFile = mGameFiles[position]
|
||||||
|
|
||||||
|
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)
|
||||||
|
animateIn.fillAfter = true
|
||||||
|
val animateOut = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_out)
|
||||||
|
animateOut.fillAfter = true
|
||||||
|
holder.apply {
|
||||||
|
if (GameFileCacheManager.findSecondDisc(gameFile) != null) {
|
||||||
|
binding.textGameCaption.text =
|
||||||
|
context.getString(R.string.disc_number, gameFile.discNumber + 1)
|
||||||
|
} else {
|
||||||
|
binding.textGameCaption.text = gameFile.company
|
||||||
|
}
|
||||||
|
holder.gameFile = gameFile
|
||||||
|
binding.root.onFocusChangeListener =
|
||||||
|
View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
|
||||||
|
binding.cardGameArt.startAnimation(if (hasFocus) animateIn else animateOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameViewHolder(var binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
var gameFile: GameFile? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.root.tag = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the LayoutManager to find out how much data we have.
|
||||||
|
*
|
||||||
|
* @return Size of the dataset.
|
||||||
|
*/
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return mGameFiles.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell Android whether or not each item in the dataset has a stable identifier.
|
||||||
|
*
|
||||||
|
* @param hasStableIds ignored.
|
||||||
|
*/
|
||||||
|
override fun setHasStableIds(hasStableIds: Boolean) {
|
||||||
|
super.setHasStableIds(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a load is finished, call this to replace the existing data
|
||||||
|
* with the newly-loaded data.
|
||||||
|
*/
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun swapDataSet(gameFiles: List<GameFile>) {
|
||||||
|
mGameFiles = gameFiles
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-fetches game metadata from the game file cache.
|
||||||
|
*/
|
||||||
|
fun refetchMetadata() {
|
||||||
|
notifyItemRangeChanged(0, itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the game that was clicked on.
|
||||||
|
*
|
||||||
|
* @param view The card representing the game the user wants to play.
|
||||||
|
*/
|
||||||
|
override fun onClick(view: View) {
|
||||||
|
val holder = view.tag as GameViewHolder
|
||||||
|
val paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile)
|
||||||
|
EmulationActivity.launch(view.context as FragmentActivity, paths, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the details activity for this Game, using an ID stored in the
|
||||||
|
* details button's Tag.
|
||||||
|
*
|
||||||
|
* @param view The Card button that was long-clicked.
|
||||||
|
*/
|
||||||
|
override fun onLongClick(view: View): Boolean {
|
||||||
|
val holder = view.tag as GameViewHolder
|
||||||
|
val fragment = GamePropertiesDialog.newInstance(holder.gameFile)
|
||||||
|
(view.context as FragmentActivity).supportFragmentManager.beginTransaction()
|
||||||
|
.add(fragment, GamePropertiesDialog.TAG).commit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -1,88 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import androidx.leanback.widget.ImageCardView;
|
|
||||||
import androidx.leanback.widget.Presenter;
|
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.R;
|
|
||||||
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog;
|
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile;
|
|
||||||
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
|
|
||||||
import org.dolphinemu.dolphinemu.utils.GlideUtils;
|
|
||||||
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Leanback library / docs call this a Presenter, but it works very
|
|
||||||
* similarly to a RecyclerView.Adapter.
|
|
||||||
*/
|
|
||||||
public final class GameRowPresenter extends Presenter
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(ViewGroup parent)
|
|
||||||
{
|
|
||||||
// Create a new view.
|
|
||||||
ImageCardView gameCard = new ImageCardView(parent.getContext());
|
|
||||||
|
|
||||||
gameCard.setMainImageAdjustViewBounds(true);
|
|
||||||
gameCard.setMainImageDimensions(240, 336);
|
|
||||||
gameCard.setMainImageScaleType(ImageView.ScaleType.CENTER_CROP);
|
|
||||||
|
|
||||||
gameCard.setFocusable(true);
|
|
||||||
gameCard.setFocusableInTouchMode(true);
|
|
||||||
|
|
||||||
// Use that view to create a ViewHolder.
|
|
||||||
return new TvGameViewHolder(gameCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(ViewHolder viewHolder, Object item)
|
|
||||||
{
|
|
||||||
TvGameViewHolder holder = (TvGameViewHolder) viewHolder;
|
|
||||||
Context context = holder.cardParent.getContext();
|
|
||||||
GameFile gameFile = (GameFile) item;
|
|
||||||
|
|
||||||
holder.imageScreenshot.setImageDrawable(null);
|
|
||||||
GlideUtils.loadGameCover(null, holder.imageScreenshot, gameFile, null);
|
|
||||||
|
|
||||||
holder.cardParent.setTitleText(gameFile.getTitle());
|
|
||||||
|
|
||||||
if (GameFileCacheManager.findSecondDisc(gameFile) != null)
|
|
||||||
{
|
|
||||||
holder.cardParent.setContentText(
|
|
||||||
context.getString(R.string.disc_number, gameFile.getDiscNumber() + 1));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
holder.cardParent.setContentText(gameFile.getCompany());
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.gameFile = gameFile;
|
|
||||||
|
|
||||||
// Set the background color of the card
|
|
||||||
Drawable background = ContextCompat.getDrawable(context, R.drawable.tv_card_background);
|
|
||||||
holder.cardParent.setInfoAreaBackground(background);
|
|
||||||
holder.cardParent.setOnLongClickListener((view) ->
|
|
||||||
{
|
|
||||||
FragmentActivity activity = (FragmentActivity) view.getContext();
|
|
||||||
GamePropertiesDialog fragment = GamePropertiesDialog.newInstance(holder.gameFile);
|
|
||||||
activity.getSupportFragmentManager().beginTransaction()
|
|
||||||
.add(fragment, GamePropertiesDialog.TAG).commit();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUnbindViewHolder(ViewHolder viewHolder)
|
|
||||||
{
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,89 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.dolphinemu.dolphinemu.adapters
|
||||||
|
|
||||||
|
import androidx.leanback.widget.Presenter
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.leanback.widget.ImageCardView
|
||||||
|
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
|
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||||
|
import org.dolphinemu.dolphinemu.R
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import android.widget.ImageView
|
||||||
|
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.utils.CoilUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Leanback library / docs call this a Presenter, but it works very
|
||||||
|
* similarly to a RecyclerView.Adapter.
|
||||||
|
*/
|
||||||
|
class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
||||||
|
// Create a new view.
|
||||||
|
val gameCard = ImageCardView(parent.context)
|
||||||
|
gameCard.apply {
|
||||||
|
setMainImageAdjustViewBounds(true)
|
||||||
|
setMainImageDimensions(240, 336)
|
||||||
|
setMainImageScaleType(ImageView.ScaleType.CENTER_CROP)
|
||||||
|
isFocusable = true
|
||||||
|
isFocusableInTouchMode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use that view to create a ViewHolder.
|
||||||
|
return TvGameViewHolder(gameCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
||||||
|
val holder = viewHolder as TvGameViewHolder
|
||||||
|
val context = holder.cardParent.context
|
||||||
|
val gameFile = item as GameFile
|
||||||
|
|
||||||
|
holder.apply {
|
||||||
|
imageScreenshot.setImageDrawable(null)
|
||||||
|
cardParent.titleText = gameFile.title
|
||||||
|
holder.gameFile = gameFile
|
||||||
|
|
||||||
|
// Set the background color of the card
|
||||||
|
val background = ContextCompat.getDrawable(context, R.drawable.tv_card_background)
|
||||||
|
cardParent.infoAreaBackground = background
|
||||||
|
cardParent.setOnClickListener { view: View ->
|
||||||
|
val activity = view.context as FragmentActivity
|
||||||
|
val fragment = GamePropertiesDialog.newInstance(holder.gameFile)
|
||||||
|
activity.supportFragmentManager.beginTransaction()
|
||||||
|
.add(fragment, GamePropertiesDialog.TAG).commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GameFileCacheManager.findSecondDisc(gameFile) != null) {
|
||||||
|
holder.cardParent.contentText =
|
||||||
|
context.getString(R.string.disc_number, gameFile.discNumber + 1)
|
||||||
|
} else {
|
||||||
|
holder.cardParent.contentText = gameFile.company
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
}
|
@ -1,172 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.dialogs;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.NativeLibrary;
|
|
||||||
import org.dolphinemu.dolphinemu.R;
|
|
||||||
import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsBinding;
|
|
||||||
import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsTvBinding;
|
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile;
|
|
||||||
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
|
|
||||||
import org.dolphinemu.dolphinemu.utils.GlideUtils;
|
|
||||||
|
|
||||||
public final class GameDetailsDialog extends DialogFragment
|
|
||||||
{
|
|
||||||
private static final String ARG_GAME_PATH = "game_path";
|
|
||||||
|
|
||||||
public static GameDetailsDialog newInstance(String gamePath)
|
|
||||||
{
|
|
||||||
GameDetailsDialog fragment = new GameDetailsDialog();
|
|
||||||
|
|
||||||
Bundle arguments = new Bundle();
|
|
||||||
arguments.putString(ARG_GAME_PATH, gamePath);
|
|
||||||
fragment.setArguments(arguments);
|
|
||||||
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState)
|
|
||||||
{
|
|
||||||
GameFile gameFile = GameFileCacheManager.addOrGet(getArguments().getString(ARG_GAME_PATH));
|
|
||||||
|
|
||||||
String country = getResources().getStringArray(R.array.countryNames)[gameFile.getCountry()];
|
|
||||||
String description = gameFile.getDescription();
|
|
||||||
String fileSize = NativeLibrary.FormatSize(gameFile.getFileSize(), 2);
|
|
||||||
|
|
||||||
// TODO: Remove dialog_game_details_tv if we switch to an AppCompatActivity for leanback
|
|
||||||
DialogGameDetailsBinding binding;
|
|
||||||
DialogGameDetailsTvBinding tvBinding;
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
|
|
||||||
if (requireActivity() instanceof AppCompatActivity)
|
|
||||||
{
|
|
||||||
binding = DialogGameDetailsBinding.inflate(getLayoutInflater());
|
|
||||||
|
|
||||||
binding.textGameTitle.setText(gameFile.getTitle());
|
|
||||||
binding.textDescription.setText(gameFile.getDescription());
|
|
||||||
if (description.isEmpty())
|
|
||||||
{
|
|
||||||
binding.textDescription.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.textCountry.setText(country);
|
|
||||||
binding.textCompany.setText(gameFile.getCompany());
|
|
||||||
binding.textGameId.setText(gameFile.getGameId());
|
|
||||||
binding.textRevision.setText(String.valueOf(gameFile.getRevision()));
|
|
||||||
|
|
||||||
if (!gameFile.shouldShowFileFormatDetails())
|
|
||||||
{
|
|
||||||
binding.labelFileFormat.setText(R.string.game_details_file_size);
|
|
||||||
binding.textFileFormat.setText(fileSize);
|
|
||||||
|
|
||||||
binding.labelCompression.setVisibility(View.GONE);
|
|
||||||
binding.textCompression.setVisibility(View.GONE);
|
|
||||||
binding.labelBlockSize.setVisibility(View.GONE);
|
|
||||||
binding.textBlockSize.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
long blockSize = gameFile.getBlockSize();
|
|
||||||
String compression = gameFile.getCompressionMethod();
|
|
||||||
|
|
||||||
binding.textFileFormat.setText(
|
|
||||||
getResources().getString(R.string.game_details_size_and_format,
|
|
||||||
gameFile.getFileFormatName(), fileSize));
|
|
||||||
|
|
||||||
if (compression.isEmpty())
|
|
||||||
{
|
|
||||||
binding.textCompression.setText(R.string.game_details_no_compression);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
binding.textCompression.setText(gameFile.getCompressionMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockSize > 0)
|
|
||||||
{
|
|
||||||
binding.textBlockSize.setText(NativeLibrary.FormatSize(blockSize, 0));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
binding.labelBlockSize.setVisibility(View.GONE);
|
|
||||||
binding.textBlockSize.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GlideUtils.loadGameBanner(binding.banner, gameFile);
|
|
||||||
|
|
||||||
builder.setView(binding.getRoot());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tvBinding = DialogGameDetailsTvBinding.inflate(getLayoutInflater());
|
|
||||||
|
|
||||||
tvBinding.textGameTitle.setText(gameFile.getTitle());
|
|
||||||
tvBinding.textDescription.setText(gameFile.getDescription());
|
|
||||||
if (description.isEmpty())
|
|
||||||
{
|
|
||||||
tvBinding.textDescription.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
tvBinding.textCountry.setText(country);
|
|
||||||
tvBinding.textCompany.setText(gameFile.getCompany());
|
|
||||||
tvBinding.textGameId.setText(gameFile.getGameId());
|
|
||||||
tvBinding.textRevision.setText(String.valueOf(gameFile.getRevision()));
|
|
||||||
|
|
||||||
if (!gameFile.shouldShowFileFormatDetails())
|
|
||||||
{
|
|
||||||
tvBinding.labelFileFormat.setText(R.string.game_details_file_size);
|
|
||||||
tvBinding.textFileFormat.setText(fileSize);
|
|
||||||
|
|
||||||
tvBinding.labelCompression.setVisibility(View.GONE);
|
|
||||||
tvBinding.textCompression.setVisibility(View.GONE);
|
|
||||||
tvBinding.labelBlockSize.setVisibility(View.GONE);
|
|
||||||
tvBinding.textBlockSize.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
long blockSize = gameFile.getBlockSize();
|
|
||||||
String compression = gameFile.getCompressionMethod();
|
|
||||||
|
|
||||||
tvBinding.textFileFormat.setText(
|
|
||||||
getResources().getString(R.string.game_details_size_and_format,
|
|
||||||
gameFile.getFileFormatName(), fileSize));
|
|
||||||
|
|
||||||
if (compression.isEmpty())
|
|
||||||
{
|
|
||||||
tvBinding.textCompression.setText(R.string.game_details_no_compression);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tvBinding.textCompression.setText(gameFile.getCompressionMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockSize > 0)
|
|
||||||
{
|
|
||||||
tvBinding.textBlockSize.setText(NativeLibrary.FormatSize(blockSize, 0));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tvBinding.labelBlockSize.setVisibility(View.GONE);
|
|
||||||
tvBinding.textBlockSize.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GlideUtils.loadGameBanner(tvBinding.banner, gameFile);
|
|
||||||
|
|
||||||
builder.setView(tvBinding.getRoot());
|
|
||||||
}
|
|
||||||
return builder.create();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,175 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.dolphinemu.dolphinemu.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||||
|
import org.dolphinemu.dolphinemu.R
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
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.databinding.DialogGameDetailsBinding
|
||||||
|
import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsTvBinding
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
|
|
||||||
|
class GameDetailsDialog : DialogFragment() {
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val gameFile = GameFileCacheManager.addOrGet(requireArguments().getString(ARG_GAME_PATH))
|
||||||
|
|
||||||
|
val country = resources.getStringArray(R.array.countryNames)[gameFile.country]
|
||||||
|
val fileSize = NativeLibrary.FormatSize(gameFile.fileSize, 2)
|
||||||
|
|
||||||
|
// TODO: Remove dialog_game_details_tv if we switch to an AppCompatActivity for leanback
|
||||||
|
val binding: DialogGameDetailsBinding
|
||||||
|
val tvBinding: DialogGameDetailsTvBinding
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
if (requireActivity() is AppCompatActivity) {
|
||||||
|
binding = DialogGameDetailsBinding.inflate(layoutInflater)
|
||||||
|
binding.apply {
|
||||||
|
textGameTitle.text = gameFile.title
|
||||||
|
textDescription.text = gameFile.description
|
||||||
|
if (gameFile.description.isEmpty()) {
|
||||||
|
textDescription.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
textCountry.text = country
|
||||||
|
textCompany.text = gameFile.company
|
||||||
|
textGameId.text = gameFile.gameId
|
||||||
|
textRevision.text = gameFile.revision.toString()
|
||||||
|
|
||||||
|
if (!gameFile.shouldShowFileFormatDetails()) {
|
||||||
|
labelFileFormat.setText(R.string.game_details_file_size)
|
||||||
|
textFileFormat.text = fileSize
|
||||||
|
|
||||||
|
labelCompression.visibility = View.GONE
|
||||||
|
textCompression.visibility = View.GONE
|
||||||
|
labelBlockSize.visibility = View.GONE
|
||||||
|
textBlockSize.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
val blockSize = gameFile.blockSize
|
||||||
|
val compression = gameFile.compressionMethod
|
||||||
|
|
||||||
|
textFileFormat.text = resources.getString(
|
||||||
|
R.string.game_details_size_and_format,
|
||||||
|
gameFile.fileFormatName,
|
||||||
|
fileSize
|
||||||
|
)
|
||||||
|
|
||||||
|
if (compression.isEmpty()) {
|
||||||
|
textCompression.setText(R.string.game_details_no_compression)
|
||||||
|
} else {
|
||||||
|
textCompression.text = gameFile.compressionMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockSize > 0) {
|
||||||
|
textBlockSize.text = NativeLibrary.FormatSize(blockSize, 0)
|
||||||
|
} else {
|
||||||
|
labelBlockSize.visibility = View.GONE
|
||||||
|
textBlockSize.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lifecycleScope.launch {
|
||||||
|
loadGameBanner(binding.banner, gameFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setView(binding.root)
|
||||||
|
} else {
|
||||||
|
tvBinding = DialogGameDetailsTvBinding.inflate(layoutInflater)
|
||||||
|
tvBinding.apply {
|
||||||
|
textGameTitle.text = gameFile.title
|
||||||
|
textDescription.text = gameFile.description
|
||||||
|
if (gameFile.description.isEmpty()) {
|
||||||
|
tvBinding.textDescription.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
textCountry.text = country
|
||||||
|
textCompany.text = gameFile.company
|
||||||
|
textGameId.text = gameFile.gameId
|
||||||
|
textRevision.text = gameFile.revision.toString()
|
||||||
|
|
||||||
|
if (!gameFile.shouldShowFileFormatDetails()) {
|
||||||
|
labelFileFormat.setText(R.string.game_details_file_size)
|
||||||
|
textFileFormat.text = fileSize
|
||||||
|
|
||||||
|
labelCompression.visibility = View.GONE
|
||||||
|
textCompression.visibility = View.GONE
|
||||||
|
labelBlockSize.visibility = View.GONE
|
||||||
|
textBlockSize.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
val blockSize = gameFile.blockSize
|
||||||
|
val compression = gameFile.compressionMethod
|
||||||
|
|
||||||
|
textFileFormat.text = resources.getString(
|
||||||
|
R.string.game_details_size_and_format,
|
||||||
|
gameFile.fileFormatName,
|
||||||
|
fileSize
|
||||||
|
)
|
||||||
|
|
||||||
|
if (compression.isEmpty()) {
|
||||||
|
textCompression.setText(R.string.game_details_no_compression)
|
||||||
|
} else {
|
||||||
|
textCompression.text = gameFile.compressionMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockSize > 0) {
|
||||||
|
textBlockSize.text = NativeLibrary.FormatSize(blockSize, 0)
|
||||||
|
} else {
|
||||||
|
labelBlockSize.visibility = View.GONE
|
||||||
|
textBlockSize.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lifecycleScope.launch {
|
||||||
|
loadGameBanner(tvBinding.banner, gameFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setView(tvBinding.root)
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
private const val ARG_GAME_PATH = "game_path"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun newInstance(gamePath: String?): GameDetailsDialog {
|
||||||
|
val fragment = GameDetailsDialog()
|
||||||
|
val arguments = Bundle()
|
||||||
|
arguments.putString(ARG_GAME_PATH, gamePath)
|
||||||
|
fragment.arguments = arguments
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
@ -74,7 +75,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
|
|||||||
NativeConfig.LAYER_BASE,
|
NativeConfig.LAYER_BASE,
|
||||||
mBindingMobile.switchDownloadCovers.isChecked
|
mBindingMobile.switchDownloadCovers.isChecked
|
||||||
)
|
)
|
||||||
mView.reloadGrid()
|
(mView as Activity).recreate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +105,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,83 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.utils;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile;
|
|
||||||
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
|
|
||||||
public final class CoverHelper
|
|
||||||
{
|
|
||||||
public static String buildGameTDBUrl(GameFile game, String region)
|
|
||||||
{
|
|
||||||
String baseUrl = "https://art.gametdb.com/wii/cover/%s/%s.png";
|
|
||||||
return String.format(baseUrl, region, game.getGameTdbId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getRegion(GameFile game)
|
|
||||||
{
|
|
||||||
String region;
|
|
||||||
switch (game.getRegion())
|
|
||||||
{
|
|
||||||
case 0: // NTSC_J
|
|
||||||
region = "JA";
|
|
||||||
break;
|
|
||||||
case 1: // NTSC_U
|
|
||||||
region = "US";
|
|
||||||
break;
|
|
||||||
case 4: // NTSC_K
|
|
||||||
region = "KO";
|
|
||||||
break;
|
|
||||||
case 2: // PAL
|
|
||||||
switch (game.getCountry())
|
|
||||||
{
|
|
||||||
case 3: // Australia
|
|
||||||
region = "AU";
|
|
||||||
break;
|
|
||||||
case 4: // France
|
|
||||||
region = "FR";
|
|
||||||
break;
|
|
||||||
case 5: // Germany
|
|
||||||
region = "DE";
|
|
||||||
break;
|
|
||||||
case 6: // Italy
|
|
||||||
region = "IT";
|
|
||||||
break;
|
|
||||||
case 8: // Netherlands
|
|
||||||
region = "NL";
|
|
||||||
break;
|
|
||||||
case 9: // Russia
|
|
||||||
region = "RU";
|
|
||||||
break;
|
|
||||||
case 10: // Spain
|
|
||||||
region = "ES";
|
|
||||||
break;
|
|
||||||
case 0: // Europe
|
|
||||||
default:
|
|
||||||
region = "EN";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 3: // Unknown
|
|
||||||
default:
|
|
||||||
region = "EN";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void saveCover(Bitmap cover, String path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
FileOutputStream out = new FileOutputStream(path);
|
|
||||||
cover.compress(Bitmap.CompressFormat.PNG, 100, out);
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
catch (Exception ignored)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.dolphinemu.dolphinemu.utils
|
||||||
|
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
|
|
||||||
|
object CoverHelper {
|
||||||
|
@JvmStatic
|
||||||
|
fun buildGameTDBUrl(game: GameFile, region: String?): String {
|
||||||
|
val baseUrl = "https://art.gametdb.com/wii/cover/%s/%s.png"
|
||||||
|
return String.format(baseUrl, region, game.gameTdbId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getRegion(game: GameFile): String {
|
||||||
|
val region: String = when (game.region) {
|
||||||
|
GameFile.REGION_NTSC_J -> "JA"
|
||||||
|
GameFile.REGION_NTSC_U -> "US"
|
||||||
|
GameFile.REGION_NTSC_K -> "KO"
|
||||||
|
GameFile.REGION_PAL -> when (game.country) {
|
||||||
|
3 -> "AU" // Australia
|
||||||
|
4 -> "FR" // France
|
||||||
|
5 -> "DE" // Germany
|
||||||
|
6 -> "IT" // Italy
|
||||||
|
8 -> "NL" // Netherlands
|
||||||
|
9 -> "RU" // Russia
|
||||||
|
10 -> "ES" // Spain
|
||||||
|
0 -> "EN" // Europe
|
||||||
|
else -> "EN"
|
||||||
|
}
|
||||||
|
3 -> "EN" // Unknown
|
||||||
|
else -> "EN"
|
||||||
|
}
|
||||||
|
return region
|
||||||
|
}
|
||||||
|
}
|
@ -1,248 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.utils;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.bumptech.glide.load.DataSource;
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
||||||
import com.bumptech.glide.load.engine.GlideException;
|
|
||||||
import com.bumptech.glide.request.RequestListener;
|
|
||||||
import com.bumptech.glide.request.target.CustomTarget;
|
|
||||||
import com.bumptech.glide.request.target.Target;
|
|
||||||
import com.bumptech.glide.request.transition.Transition;
|
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.R;
|
|
||||||
import org.dolphinemu.dolphinemu.adapters.GameAdapter;
|
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
|
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
public class GlideUtils
|
|
||||||
{
|
|
||||||
private static final ExecutorService saveCoverExecutor = Executors.newSingleThreadExecutor();
|
|
||||||
private static final ExecutorService unmangleExecutor = Executors.newSingleThreadExecutor();
|
|
||||||
private static final Handler unmangleHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
public static void loadGameBanner(ImageView imageView, GameFile gameFile)
|
|
||||||
{
|
|
||||||
Context context = imageView.getContext();
|
|
||||||
int[] vector = gameFile.getBanner();
|
|
||||||
int width = gameFile.getBannerWidth();
|
|
||||||
int height = gameFile.getBannerHeight();
|
|
||||||
if (width > 0 && height > 0)
|
|
||||||
{
|
|
||||||
Bitmap 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void loadGameCover(GameAdapter.GameViewHolder gameViewHolder, ImageView imageView,
|
|
||||||
GameFile gameFile, Activity activity)
|
|
||||||
{
|
|
||||||
if (BooleanSetting.MAIN_SHOW_GAME_TITLES.getBooleanGlobal() && gameViewHolder != null)
|
|
||||||
{
|
|
||||||
gameViewHolder.binding.textGameTitle.setText(gameFile.getTitle());
|
|
||||||
gameViewHolder.binding.textGameTitle.setVisibility(View.VISIBLE);
|
|
||||||
gameViewHolder.binding.textGameTitleInner.setVisibility(View.GONE);
|
|
||||||
gameViewHolder.binding.textGameCaption.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
else if (gameViewHolder != null)
|
|
||||||
{
|
|
||||||
gameViewHolder.binding.textGameTitleInner.setText(gameFile.getTitle());
|
|
||||||
gameViewHolder.binding.textGameTitle.setVisibility(View.GONE);
|
|
||||||
gameViewHolder.binding.textGameCaption.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
unmangleExecutor.execute(() ->
|
|
||||||
{
|
|
||||||
String customCoverPath = gameFile.getCustomCoverPath();
|
|
||||||
Uri customCoverUri = null;
|
|
||||||
boolean customCoverExists = false;
|
|
||||||
if (ContentHandler.isContentUri(customCoverPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
customCoverUri = ContentHandler.unmangle(customCoverPath);
|
|
||||||
customCoverExists = true;
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException | SecurityException ignored)
|
|
||||||
{
|
|
||||||
// Let customCoverExists remain false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
customCoverUri = Uri.parse(customCoverPath);
|
|
||||||
customCoverExists = new File(customCoverPath).exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
Context context = imageView.getContext();
|
|
||||||
boolean finalCustomCoverExists = customCoverExists;
|
|
||||||
Uri finalCustomCoverUri = customCoverUri;
|
|
||||||
|
|
||||||
File cover = new File(gameFile.getCoverPath(context));
|
|
||||||
boolean 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalCustomCoverExists)
|
|
||||||
{
|
|
||||||
Glide.with(imageView)
|
|
||||||
.load(finalCustomCoverUri)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.centerCrop()
|
|
||||||
.error(R.drawable.no_banner)
|
|
||||||
.listener(new RequestListener<Drawable>()
|
|
||||||
{
|
|
||||||
@Override public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
|
||||||
Target<Drawable> target, boolean isFirstResource)
|
|
||||||
{
|
|
||||||
GlideUtils.enableInnerTitle(gameViewHolder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean onResourceReady(Drawable resource, Object model,
|
|
||||||
Target<Drawable> target, DataSource dataSource, boolean isFirstResource)
|
|
||||||
{
|
|
||||||
GlideUtils.disableInnerTitle(gameViewHolder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(imageView);
|
|
||||||
}
|
|
||||||
else if (cachedCoverExists)
|
|
||||||
{
|
|
||||||
Glide.with(imageView)
|
|
||||||
.load(cover)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.centerCrop()
|
|
||||||
.error(R.drawable.no_banner)
|
|
||||||
.listener(new RequestListener<Drawable>()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
|
||||||
Target<Drawable> target, boolean isFirstResource)
|
|
||||||
{
|
|
||||||
GlideUtils.enableInnerTitle(gameViewHolder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onResourceReady(Drawable resource, Object model,
|
|
||||||
Target<Drawable> target, DataSource dataSource, boolean isFirstResource)
|
|
||||||
{
|
|
||||||
GlideUtils.disableInnerTitle(gameViewHolder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(imageView);
|
|
||||||
}
|
|
||||||
else if (BooleanSetting.MAIN_USE_GAME_COVERS.getBooleanGlobal())
|
|
||||||
{
|
|
||||||
Glide.with(context)
|
|
||||||
.load(CoverHelper.buildGameTDBUrl(gameFile, CoverHelper.getRegion(gameFile)))
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.centerCrop()
|
|
||||||
.error(R.drawable.no_banner)
|
|
||||||
.listener(new RequestListener<Drawable>()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public boolean onLoadFailed(@Nullable GlideException e, Object model,
|
|
||||||
Target<Drawable> target, boolean isFirstResource)
|
|
||||||
{
|
|
||||||
GlideUtils.enableInnerTitle(gameViewHolder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onResourceReady(Drawable resource, Object model,
|
|
||||||
Target<Drawable> target, DataSource dataSource, boolean isFirstResource)
|
|
||||||
{
|
|
||||||
GlideUtils.disableInnerTitle(gameViewHolder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(new CustomTarget<Drawable>()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void onResourceReady(@NonNull Drawable resource,
|
|
||||||
@Nullable Transition<? super Drawable> transition)
|
|
||||||
{
|
|
||||||
Bitmap cover = ((BitmapDrawable) resource).getBitmap();
|
|
||||||
saveCoverExecutor.execute(
|
|
||||||
() -> CoverHelper.saveCover(cover, gameFile.getCoverPath(context)));
|
|
||||||
imageView.setImageBitmap(cover);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadCleared(@Nullable Drawable placeholder)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Glide.with(imageView.getContext())
|
|
||||||
.load(R.drawable.no_banner)
|
|
||||||
.into(imageView);
|
|
||||||
enableInnerTitle(gameViewHolder);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void enableInnerTitle(GameAdapter.GameViewHolder gameViewHolder)
|
|
||||||
{
|
|
||||||
if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.getBooleanGlobal())
|
|
||||||
{
|
|
||||||
gameViewHolder.binding.textGameTitleInner.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void disableInnerTitle(GameAdapter.GameViewHolder gameViewHolder)
|
|
||||||
{
|
|
||||||
if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.getBooleanGlobal())
|
|
||||||
{
|
|
||||||
gameViewHolder.binding.textGameTitleInner.setVisibility(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);
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.viewholders;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import androidx.leanback.widget.ImageCardView;
|
|
||||||
import androidx.leanback.widget.Presenter;
|
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.model.GameFile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple class that stores references to views so that the GameAdapter doesn't need to
|
|
||||||
* keep calling findViewById(), which is expensive.
|
|
||||||
*/
|
|
||||||
public final class TvGameViewHolder extends Presenter.ViewHolder
|
|
||||||
{
|
|
||||||
public ImageCardView cardParent;
|
|
||||||
|
|
||||||
public ImageView imageScreenshot;
|
|
||||||
|
|
||||||
public GameFile gameFile;
|
|
||||||
|
|
||||||
public TvGameViewHolder(View itemView)
|
|
||||||
{
|
|
||||||
super(itemView);
|
|
||||||
|
|
||||||
itemView.setTag(this);
|
|
||||||
|
|
||||||
cardParent = (ImageCardView) itemView;
|
|
||||||
imageScreenshot = cardParent.getMainImageView();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.dolphinemu.dolphinemu.viewholders
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.leanback.widget.Presenter
|
||||||
|
import androidx.leanback.widget.ImageCardView
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple class that stores references to views so that the GameAdapter doesn't need to
|
||||||
|
* keep calling findViewById(), which is expensive.
|
||||||
|
*/
|
||||||
|
class TvGameViewHolder(itemView: View) : Presenter.ViewHolder(itemView) {
|
||||||
|
var cardParent: ImageCardView
|
||||||
|
var imageScreenshot: ImageView
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var gameFile: GameFile? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.tag = this
|
||||||
|
cardParent = itemView as ImageCardView
|
||||||
|
imageScreenshot = cardParent.mainImageView
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user