| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |