Merge pull request #11408 from t895/coil
Android: Rewrite image loading with Kotlin and Coil
This commit is contained in:
commit
0fb9105700
|
@ -155,7 +155,7 @@ dependencies {
|
|||
implementation 'com.android.volley:volley:1.2.1'
|
||||
|
||||
// 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'
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
import android.app.Activity
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
|
@ -74,7 +75,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
|
|||
NativeConfig.LAYER_BASE,
|
||||
mBindingMobile.switchDownloadCovers.isChecked
|
||||
)
|
||||
mView.reloadGrid()
|
||||
(mView as Activity).recreate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +105,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
|
|||
NativeConfig.LAYER_BASE,
|
||||
mBindingTv.switchDownloadCovers.isChecked
|
||||
)
|
||||
mView.reloadGrid()
|
||||
(mView as Activity).recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
package org.dolphinemu.dolphinemu.model;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
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
|
||||
private long mPointer;
|
||||
|
||||
|
@ -68,11 +71,6 @@ public class GameFile
|
|||
|
||||
public native int getBannerHeight();
|
||||
|
||||
public String getCoverPath(Context context)
|
||||
{
|
||||
return context.getExternalCacheDir().getPath() + "/GameCovers/" + getGameTdbId() + ".png";
|
||||
}
|
||||
|
||||
public String getCustomCoverPath()
|
||||
{
|
||||
return getPath().substring(0, getPath().lastIndexOf(".")) + ".cover.png";
|
||||
|
|
|
@ -349,7 +349,7 @@ public final class TvMainActivity extends FragmentActivity
|
|||
}
|
||||
|
||||
// Create an adapter for this row.
|
||||
ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter());
|
||||
ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter(this));
|
||||
row.addAll(0, gameFiles);
|
||||
|
||||
// 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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue