This file was deleted.

@@ -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
}
}

This file was deleted.

@@ -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
Expand Down Expand Up @@ -74,7 +75,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
NativeConfig.LAYER_BASE,
mBindingMobile.switchDownloadCovers.isChecked
)
mView.reloadGrid()
(mView as Activity).recreate()
}
}

Expand Down Expand Up @@ -104,7 +105,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
NativeConfig.LAYER_BASE,
mBindingTv.switchDownloadCovers.isChecked
)
mView.reloadGrid()
(mView as Activity).recreate()
}
}
}
Expand Up @@ -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;

Expand Down Expand Up @@ -68,11 +71,6 @@ private GameFile(long pointer)

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";
Expand Down
Expand Up @@ -349,7 +349,7 @@ private ListRow buildGamesRow(Platform platform, Collection<GameFile> gameFiles)
}

// 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.
Expand Down
@@ -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
}
}
}

This file was deleted.

@@ -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
}
}

This file was deleted.

Expand Up @@ -186,9 +186,9 @@ public static Uri buildBanner(GameFile game, Context context)
}
}

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);
Expand Down

This file was deleted.

@@ -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
}
}