diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java deleted file mode 100644 index fb9fd356da..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java +++ /dev/null @@ -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 implements - View.OnClickListener, - View.OnLongClickListener -{ - private List 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 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; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt new file mode 100644 index 0000000000..e3fa287967 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.adapters + +import android.annotation.SuppressLint +import android.app.Activity +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.utils.GlideUtils +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 org.dolphinemu.dolphinemu.databinding.CardGameBinding +import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog +import java.util.ArrayList + +class GameAdapter(activity: Activity) : RecyclerView.Adapter(), + View.OnClickListener, OnLongClickListener { + private var mGameFiles: List + 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. + * + * @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] + + GlideUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile, mActivity) + + 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(binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) { + var gameFile: GameFile? = null + + @JvmField + var binding: CardGameBinding + + init { + binding.root.tag = this + this.binding = binding + } + } + + /** + * 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) { + 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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java deleted file mode 100644 index 53b0e56861..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java +++ /dev/null @@ -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 - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt new file mode 100644 index 0000000000..66a249e50d --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt @@ -0,0 +1,72 @@ +// 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.utils.GlideUtils +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 org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog + +/** + * The Leanback library / docs call this a Presenter, but it works very + * similarly to a RecyclerView.Adapter. + */ +class GameRowPresenter : 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 + } + } + GlideUtils.loadGameCover(null, holder.imageScreenshot, gameFile, null) + } + + override fun onUnbindViewHolder(viewHolder: ViewHolder) { + // no op + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java deleted file mode 100644 index 176f5b7610..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java +++ /dev/null @@ -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(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt new file mode 100644 index 0000000000..5c7cf2ed6a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt @@ -0,0 +1,145 @@ +// 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 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 org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsBinding +import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsTvBinding +import org.dolphinemu.dolphinemu.utils.GlideUtils + +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 + } + } + } + + GlideUtils.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 + } + } + } + + GlideUtils.loadGameBanner(tvBinding.banner, gameFile) + + builder.setView(tvBinding.root) + } + return builder.create() + } + + 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 + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.java deleted file mode 100644 index 84ed1be6c5..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.java +++ /dev/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) - { - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt new file mode 100644 index 0000000000..f1deeb3730 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import org.dolphinemu.dolphinemu.model.GameFile +import android.graphics.Bitmap +import java.io.FileOutputStream +import java.lang.Exception + +object CoverHelper { + 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) + } + + fun getRegion(game: GameFile): String { + val region: String = when (game.region) { + 0 -> "JA" + 1 -> "US" + 4 -> "KO" + 2 -> when (game.country) { + 3 -> "AU" + 4 -> "FR" + 5 -> "DE" + 6 -> "IT" + 8 -> "NL" + 9 -> "RU" + 10 -> "ES" + 0 -> "EN" + else -> "EN" + } + 3 -> "EN" + else -> "EN" + } + 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) { + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.java deleted file mode 100644 index 766d9684dc..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.java +++ /dev/null @@ -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() - { - @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, - Target target, boolean isFirstResource) - { - GlideUtils.enableInnerTitle(gameViewHolder); - return false; - } - - @Override public boolean onResourceReady(Drawable resource, Object model, - Target 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() - { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, - Target target, boolean isFirstResource) - { - GlideUtils.enableInnerTitle(gameViewHolder); - return false; - } - - @Override - public boolean onResourceReady(Drawable resource, Object model, - Target 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() - { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, - Target target, boolean isFirstResource) - { - GlideUtils.enableInnerTitle(gameViewHolder); - return false; - } - - @Override - public boolean onResourceReady(Drawable resource, Object model, - Target target, DataSource dataSource, boolean isFirstResource) - { - GlideUtils.disableInnerTitle(gameViewHolder); - return false; - } - }) - .into(new CustomTarget() - { - @Override - public void onResourceReady(@NonNull Drawable resource, - @Nullable Transition 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); - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt new file mode 100644 index 0000000000..ba2b9e344f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt @@ -0,0 +1,233 @@ +// 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 { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + enableInnerTitle(gameViewHolder) + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any, + target: Target, + 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 { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + enableInnerTitle(gameViewHolder) + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any, + target: Target, + 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 { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + enableInnerTitle(gameViewHolder) + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + disableInnerTitle(gameViewHolder) + return false + } + }) + .into(object : CustomTarget() { + override fun onLoadCleared(placeholder: Drawable?) {} + override fun onResourceReady( + resource: Drawable, + transition: Transition? + ) { + 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 + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java deleted file mode 100644 index b51caa7707..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java +++ /dev/null @@ -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(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt new file mode 100644 index 0000000000..6fc5adbfa7 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt @@ -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 + } +}