mirror of
https://git.suyu.dev/suyu/suyu
synced 2024-12-27 11:52:31 -06:00
android: Add Game properties
This commit has the UI for viewing a game's properties on long-press and some links to useful tools like - Game info - Shortcut to settings (global in this commit) - Addon manager with installer - Save data manager - Option to clear all save data - Option to clear shader cache
This commit is contained in:
parent
6b5fb2063f
commit
e975f3cde9
40 changed files with 2245 additions and 271 deletions
|
@ -230,8 +230,6 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun onTouchReleased(finger_id: Int)
|
external fun onTouchReleased(finger_id: Int)
|
||||||
|
|
||||||
external fun initGameIni(gameID: String?)
|
|
||||||
|
|
||||||
external fun setAppDirectory(directory: String)
|
external fun setAppDirectory(directory: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -241,6 +239,8 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun installFileToNand(filename: String, extension: String): Int
|
external fun installFileToNand(filename: String, extension: String): Int
|
||||||
|
|
||||||
|
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
|
||||||
|
|
||||||
external fun initializeGpuDriver(
|
external fun initializeGpuDriver(
|
||||||
hookLibDir: String?,
|
hookLibDir: String?,
|
||||||
customDriverDir: String?,
|
customDriverDir: String?,
|
||||||
|
@ -252,18 +252,11 @@ object NativeLibrary {
|
||||||
|
|
||||||
external fun initializeSystem(reload: Boolean)
|
external fun initializeSystem(reload: Boolean)
|
||||||
|
|
||||||
external fun defaultCPUCore(): Int
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begins emulation.
|
* Begins emulation.
|
||||||
*/
|
*/
|
||||||
external fun run(path: String?)
|
external fun run(path: String?)
|
||||||
|
|
||||||
/**
|
|
||||||
* Begins emulation from the specified savestate.
|
|
||||||
*/
|
|
||||||
external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
|
|
||||||
|
|
||||||
// Surface Handling
|
// Surface Handling
|
||||||
external fun surfaceChanged(surf: Surface?)
|
external fun surfaceChanged(surf: Surface?)
|
||||||
|
|
||||||
|
@ -304,10 +297,9 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun getCpuBackend(): String
|
external fun getCpuBackend(): String
|
||||||
|
|
||||||
/**
|
external fun applySettings()
|
||||||
* Notifies the core emulation that the orientation has changed.
|
|
||||||
*/
|
external fun logSettings()
|
||||||
external fun notifyOrientationChange(layout_option: Int, rotation: Int)
|
|
||||||
|
|
||||||
enum class CoreError {
|
enum class CoreError {
|
||||||
ErrorSystemFiles,
|
ErrorSystemFiles,
|
||||||
|
@ -538,6 +530,23 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun isFirmwareAvailable(): Boolean
|
external fun isFirmwareAvailable(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the PatchManager for any addons that are available
|
||||||
|
*
|
||||||
|
* @param path Path to game file. Can be a [Uri].
|
||||||
|
* @param programId String representation of a game's program ID
|
||||||
|
* @return Array of pairs where the first value is the name of an addon and the second is the version
|
||||||
|
*/
|
||||||
|
external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the save location for a specific game
|
||||||
|
*
|
||||||
|
* @param programId String representation of a game's program ID
|
||||||
|
* @return Save data path that may not exist yet
|
||||||
|
*/
|
||||||
|
external fun getSavePath(programId: String): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button type for use in onTouchEvent
|
* Button type for use in onTouchEvent
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.Addon
|
||||||
|
|
||||||
|
class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>(
|
||||||
|
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||||
|
) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
|
||||||
|
ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
.also { return AddonViewHolder(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = currentList.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: AddonViewHolder, position: Int) =
|
||||||
|
holder.bind(currentList[position])
|
||||||
|
|
||||||
|
inner class AddonViewHolder(val binding: ListItemAddonBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(addon: Addon) {
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
|
||||||
|
}
|
||||||
|
binding.title.text = addon.title
|
||||||
|
binding.version.text = addon.version
|
||||||
|
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
|
||||||
|
addon.enabled = checked
|
||||||
|
}
|
||||||
|
binding.addonSwitch.isChecked = addon.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<Addon>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding
|
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
|
||||||
import org.yuzu.yuzu_emu.model.Applet
|
import org.yuzu.yuzu_emu.model.Applet
|
||||||
import org.yuzu.yuzu_emu.model.AppletInfo
|
import org.yuzu.yuzu_emu.model.AppletInfo
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: Int
|
viewType: Int
|
||||||
): AppletAdapter.AppletViewHolder {
|
): AppletAdapter.AppletViewHolder {
|
||||||
CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
.apply { root.setOnClickListener(this@AppletAdapter) }
|
.apply { root.setOnClickListener(this@AppletAdapter) }
|
||||||
.also { return AppletViewHolder(it) }
|
.also { return AppletViewHolder(it) }
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
|
||||||
view.findNavController().navigate(action)
|
view.findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AppletViewHolder(val binding: CardAppletOptionBinding) :
|
inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
lateinit var applet: Applet
|
lateinit var applet: Applet
|
||||||
|
|
||||||
|
|
|
@ -44,19 +44,20 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||||
|
|
||||||
class GameAdapter(private val activity: AppCompatActivity) :
|
class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||||
View.OnClickListener {
|
View.OnClickListener,
|
||||||
|
View.OnLongClickListener {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||||
// Create a new view.
|
// Create a new view.
|
||||||
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
binding.cardGame.setOnClickListener(this)
|
binding.cardGame.setOnClickListener(this)
|
||||||
|
binding.cardGame.setOnLongClickListener(this)
|
||||||
|
|
||||||
// Use that view to create a ViewHolder.
|
// Use that view to create a ViewHolder.
|
||||||
return GameViewHolder(binding)
|
return GameViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
|
||||||
holder.bind(currentList[position])
|
holder.bind(currentList[position])
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = currentList.size
|
override fun getItemCount(): Int = currentList.size
|
||||||
|
|
||||||
|
@ -125,10 +126,17 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
|
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
|
||||||
view.findNavController().navigate(action)
|
view.findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLongClick(view: View): Boolean {
|
||||||
|
val holder = view.tag as GameViewHolder
|
||||||
|
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
|
||||||
|
view.findNavController().navigate(action)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
inner class GameViewHolder(val binding: CardGameBinding) :
|
inner class GameViewHolder(val binding: CardGameBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
lateinit var game: Game
|
lateinit var game: Game
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
|
||||||
|
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.GameProperty
|
||||||
|
import org.yuzu.yuzu_emu.model.InstallableProperty
|
||||||
|
import org.yuzu.yuzu_emu.model.SubmenuProperty
|
||||||
|
|
||||||
|
class GamePropertiesAdapter(
|
||||||
|
private val viewLifecycle: LifecycleOwner,
|
||||||
|
private var properties: List<GameProperty>
|
||||||
|
) :
|
||||||
|
RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() {
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): GamePropertyViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
return when (viewType) {
|
||||||
|
PropertyType.Submenu.ordinal -> {
|
||||||
|
SubmenuPropertyViewHolder(
|
||||||
|
CardSimpleOutlinedBinding.inflate(
|
||||||
|
inflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> InstallablePropertyViewHolder(
|
||||||
|
CardInstallableBinding.inflate(
|
||||||
|
inflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = properties.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) =
|
||||||
|
holder.bind(properties[position])
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when (properties[position]) {
|
||||||
|
is SubmenuProperty -> PropertyType.Submenu.ordinal
|
||||||
|
else -> PropertyType.Installable.ordinal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
abstract fun bind(property: GameProperty)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) :
|
||||||
|
GamePropertyViewHolder(binding.root) {
|
||||||
|
override fun bind(property: GameProperty) {
|
||||||
|
val submenuProperty = property as SubmenuProperty
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
submenuProperty.action.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.title.setText(submenuProperty.titleId)
|
||||||
|
binding.description.setText(submenuProperty.descriptionId)
|
||||||
|
binding.icon.setImageDrawable(
|
||||||
|
ResourcesCompat.getDrawable(
|
||||||
|
binding.icon.context.resources,
|
||||||
|
submenuProperty.iconId,
|
||||||
|
binding.icon.context.theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.details.postDelayed({
|
||||||
|
binding.details.isSelected = true
|
||||||
|
binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
if (submenuProperty.details != null) {
|
||||||
|
binding.details.visibility = View.VISIBLE
|
||||||
|
binding.details.text = submenuProperty.details.invoke()
|
||||||
|
} else if (submenuProperty.detailsFlow != null) {
|
||||||
|
binding.details.visibility = View.VISIBLE
|
||||||
|
viewLifecycle.lifecycleScope.launch {
|
||||||
|
viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
submenuProperty.detailsFlow.collect { binding.details.text = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.details.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class InstallablePropertyViewHolder(val binding: CardInstallableBinding) :
|
||||||
|
GamePropertyViewHolder(binding.root) {
|
||||||
|
override fun bind(property: GameProperty) {
|
||||||
|
val installableProperty = property as InstallableProperty
|
||||||
|
|
||||||
|
binding.title.setText(installableProperty.titleId)
|
||||||
|
binding.description.setText(installableProperty.descriptionId)
|
||||||
|
|
||||||
|
if (installableProperty.install != null) {
|
||||||
|
binding.buttonInstall.visibility = View.VISIBLE
|
||||||
|
binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() }
|
||||||
|
}
|
||||||
|
if (installableProperty.export != null) {
|
||||||
|
binding.buttonExport.visibility = View.VISIBLE
|
||||||
|
binding.buttonExport.setOnClickListener { installableProperty.export.invoke() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PropertyType {
|
||||||
|
Submenu,
|
||||||
|
Installable
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.adapters.AddonAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentAddonsBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.utils.AddonUtil
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AddonsFragment : Fragment() {
|
||||||
|
private var _binding: FragmentAddonsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val addonViewModel: AddonViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val args by navArgs<AddonsFragmentArgs>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
addonViewModel.onOpenAddons(args.game)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentAddonsBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is using the correct scope, lint is just acting up
|
||||||
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(false)
|
||||||
|
|
||||||
|
binding.toolbarAddons.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title)
|
||||||
|
|
||||||
|
binding.listAddons.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
adapter = AddonAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
addonViewModel.addonList.collect {
|
||||||
|
(binding.listAddons.adapter as AddonAdapter).submitList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
addonViewModel.showModInstallPicker.collect {
|
||||||
|
if (it) {
|
||||||
|
installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||||
|
addonViewModel.showModInstallPicker(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
addonViewModel.showModNoticeDialog.collect {
|
||||||
|
if (it) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.addon_notice,
|
||||||
|
descriptionId = R.string.addon_notice_description,
|
||||||
|
positiveAction = { addonViewModel.showModInstallPicker(true) }
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
addonViewModel.showModNoticeDialog(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonInstall.setOnClickListener {
|
||||||
|
ContentTypeSelectionDialogFragment().show(
|
||||||
|
parentFragmentManager,
|
||||||
|
ContentTypeSelectionDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
addonViewModel.refreshAddons()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
addonViewModel.onCloseAddons()
|
||||||
|
}
|
||||||
|
|
||||||
|
val installAddon =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result)
|
||||||
|
if (externalAddonDirectory == null) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.invalid_directory,
|
||||||
|
descriptionId = R.string.invalid_directory_description
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val isValid = externalAddonDirectory.listFiles()
|
||||||
|
.any { AddonUtil.validAddonDirectories.contains(it.name) }
|
||||||
|
val errorMessage = MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.invalid_directory,
|
||||||
|
descriptionId = R.string.invalid_directory_description
|
||||||
|
)
|
||||||
|
if (isValid) {
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
R.string.installing_game_content,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
val parentDirectoryName = externalAddonDirectory.name
|
||||||
|
val internalAddonDirectory =
|
||||||
|
File(args.game.addonDir + parentDirectoryName)
|
||||||
|
try {
|
||||||
|
externalAddonDirectory.copyFilesTo(internalAddonDirectory)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return@newInstance errorMessage
|
||||||
|
}
|
||||||
|
addonViewModel.refreshAddons()
|
||||||
|
return@newInstance getString(R.string.addon_installed_successfully)
|
||||||
|
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
} else {
|
||||||
|
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpToolbar = binding.toolbarAddons.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpToolbar.leftMargin = leftInsets
|
||||||
|
mlpToolbar.rightMargin = rightInsets
|
||||||
|
binding.toolbarAddons.layoutParams = mlpToolbar
|
||||||
|
|
||||||
|
val mlpAddonsList = binding.listAddons.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpAddonsList.leftMargin = leftInsets
|
||||||
|
mlpAddonsList.rightMargin = rightInsets
|
||||||
|
binding.listAddons.layoutParams = mlpAddonsList
|
||||||
|
binding.listAddons.updatePadding(
|
||||||
|
bottom = barInsets.bottom +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||||
|
)
|
||||||
|
|
||||||
|
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||||
|
val mlpFab =
|
||||||
|
binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpFab.leftMargin = leftInsets + fabSpacing
|
||||||
|
mlpFab.rightMargin = rightInsets + fabSpacing
|
||||||
|
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
|
||||||
|
binding.buttonInstall.layoutParams = mlpFab
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
|
|
||||||
|
class ContentTypeSelectionDialogFragment : DialogFragment() {
|
||||||
|
private val addonViewModel: AddonViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val preferences get() =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
|
||||||
|
private var selectedItem = 0
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val launchOptions =
|
||||||
|
arrayOf(getString(R.string.updates_and_dlc), getString(R.string.mods_and_cheats))
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mainActivity = requireActivity() as MainActivity
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.select_content_type)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
when (selectedItem) {
|
||||||
|
0 -> mainActivity.installGameUpdate.launch(arrayOf("*/*"))
|
||||||
|
else -> {
|
||||||
|
if (!preferences.getBoolean(MOD_NOTICE_SEEN, false)) {
|
||||||
|
preferences.edit().putBoolean(MOD_NOTICE_SEEN, true).apply()
|
||||||
|
addonViewModel.showModNoticeDialog(true)
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
addonViewModel.showModInstallPicker(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
|
||||||
|
selectedItem = i
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(SELECTED_ITEM, selectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "ContentTypeSelectionDialogFragment"
|
||||||
|
|
||||||
|
private const val SELECTED_ITEM = "SelectedItem"
|
||||||
|
private const val MOD_NOTICE_SEEN = "ModNoticeSeen"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||||
|
|
||||||
|
class GameInfoFragment : Fragment() {
|
||||||
|
private var _binding: FragmentGameInfoBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val args by navArgs<GameInfoFragmentArgs>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
|
||||||
|
// Check for an up-to-date version string
|
||||||
|
args.game.version = GameMetadata.getVersion(args.game.path, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentGameInfoBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(false)
|
||||||
|
|
||||||
|
binding.apply {
|
||||||
|
toolbarInfo.title = args.game.title
|
||||||
|
toolbarInfo.setNavigationOnClickListener {
|
||||||
|
view.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
val pathString = Uri.parse(args.game.path).path ?: ""
|
||||||
|
path.setHint(R.string.path)
|
||||||
|
pathField.setText(pathString)
|
||||||
|
pathField.setOnClickListener { copyToClipboard(getString(R.string.path), pathString) }
|
||||||
|
|
||||||
|
programId.setHint(R.string.program_id)
|
||||||
|
programIdField.setText(args.game.programIdHex)
|
||||||
|
programIdField.setOnClickListener {
|
||||||
|
copyToClipboard(getString(R.string.program_id), args.game.programIdHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.game.developer.isNotEmpty()) {
|
||||||
|
developer.setHint(R.string.developer)
|
||||||
|
developerField.setText(args.game.developer)
|
||||||
|
developerField.setOnClickListener {
|
||||||
|
copyToClipboard(getString(R.string.developer), args.game.developer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
developer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
version.setHint(R.string.version)
|
||||||
|
versionField.setText(args.game.version)
|
||||||
|
versionField.setOnClickListener {
|
||||||
|
copyToClipboard(getString(R.string.version), args.game.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonCopy.setOnClickListener {
|
||||||
|
val details = """
|
||||||
|
${args.game.title}
|
||||||
|
${getString(R.string.path)} - $pathString
|
||||||
|
${getString(R.string.program_id)} - ${args.game.programIdHex}
|
||||||
|
${getString(R.string.developer)} - ${args.game.developer}
|
||||||
|
${getString(R.string.version)} - ${args.game.version}
|
||||||
|
""".trimIndent()
|
||||||
|
copyToClipboard(args.game.title, details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyToClipboard(label: String, body: String) {
|
||||||
|
val clipBoard =
|
||||||
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText(label, body)
|
||||||
|
clipBoard.setPrimaryClip(clip)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.copied_to_clipboard,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpToolbar = binding.toolbarInfo.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpToolbar.leftMargin = leftInsets
|
||||||
|
mlpToolbar.rightMargin = rightInsets
|
||||||
|
binding.toolbarInfo.layoutParams = mlpToolbar
|
||||||
|
|
||||||
|
val mlpScrollAbout = binding.scrollInfo.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpScrollAbout.leftMargin = leftInsets
|
||||||
|
mlpScrollAbout.rightMargin = rightInsets
|
||||||
|
binding.scrollInfo.layoutParams = mlpScrollAbout
|
||||||
|
|
||||||
|
binding.contentInfo.updatePadding(bottom = barInsets.bottom)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,418 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
|
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.GameProperty
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.InstallableProperty
|
||||||
|
import org.yuzu.yuzu_emu.model.SubmenuProperty
|
||||||
|
import org.yuzu.yuzu_emu.model.TaskState
|
||||||
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||||
|
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||||
|
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||||
|
import org.yuzu.yuzu_emu.utils.MemoryUtil
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class GamePropertiesFragment : Fragment() {
|
||||||
|
private var _binding: FragmentGamePropertiesBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
private val driverViewModel: DriverViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val args by navArgs<GamePropertiesFragmentArgs>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentGamePropertiesBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(true)
|
||||||
|
|
||||||
|
binding.buttonBack.setOnClickListener {
|
||||||
|
view.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
|
||||||
|
binding.title.text = args.game.title
|
||||||
|
binding.title.postDelayed(
|
||||||
|
{
|
||||||
|
binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
binding.title.isSelected = true
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.buttonStart.setOnClickListener {
|
||||||
|
LaunchGameDialogFragment.newInstance(args.game)
|
||||||
|
.show(childFragmentManager, LaunchGameDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadList()
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
homeViewModel.openImportSaves.collect {
|
||||||
|
if (it) {
|
||||||
|
importSaves.launch(arrayOf("application/zip"))
|
||||||
|
homeViewModel.setOpenImportSaves(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
gamesViewModel.reloadGames(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reloadList() {
|
||||||
|
_binding ?: return
|
||||||
|
|
||||||
|
driverViewModel.updateDriverNameForGame(args.game)
|
||||||
|
val properties = mutableListOf<GameProperty>().apply {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.info,
|
||||||
|
R.string.info_description,
|
||||||
|
R.drawable.ic_info_outline
|
||||||
|
) {
|
||||||
|
val action = GamePropertiesFragmentDirections
|
||||||
|
.actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.preferences_settings,
|
||||||
|
R.string.per_game_settings_description,
|
||||||
|
R.drawable.ic_settings
|
||||||
|
) {
|
||||||
|
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||||
|
args.game,
|
||||||
|
Settings.MenuTag.SECTION_ROOT
|
||||||
|
)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!args.game.isHomebrew) {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.add_ons,
|
||||||
|
R.string.add_ons_description,
|
||||||
|
R.drawable.ic_edit
|
||||||
|
) {
|
||||||
|
val action = GamePropertiesFragmentDirections
|
||||||
|
.actionPerGamePropertiesFragmentToAddonsFragment(args.game)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
InstallableProperty(
|
||||||
|
R.string.save_data,
|
||||||
|
R.string.save_data_description,
|
||||||
|
{
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.import_save_warning,
|
||||||
|
descriptionId = R.string.import_save_warning_description,
|
||||||
|
positiveAction = { homeViewModel.setOpenImportSaves(true) }
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
},
|
||||||
|
if (File(args.game.saveDir).exists()) {
|
||||||
|
{ exportSaves.launch(args.game.saveZipName) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val saveDirFile = File(args.game.saveDir)
|
||||||
|
if (saveDirFile.exists()) {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.delete_save_data,
|
||||||
|
R.string.delete_save_data_description,
|
||||||
|
R.drawable.ic_delete,
|
||||||
|
action = {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.delete_save_data,
|
||||||
|
descriptionId = R.string.delete_save_data_warning_description,
|
||||||
|
positiveAction = {
|
||||||
|
File(args.game.saveDir).deleteRecursively()
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
R.string.save_data_deleted_successfully,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
reloadList()
|
||||||
|
}
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shaderCacheDir = File(
|
||||||
|
DirectoryInitialization.userDirectory +
|
||||||
|
"/shader/" + args.game.settingsName.lowercase()
|
||||||
|
)
|
||||||
|
if (shaderCacheDir.exists()) {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.clear_shader_cache,
|
||||||
|
R.string.clear_shader_cache_description,
|
||||||
|
R.drawable.ic_delete,
|
||||||
|
{
|
||||||
|
if (shaderCacheDir.exists()) {
|
||||||
|
val bytes = shaderCacheDir.walkTopDown().filter { it.isFile }
|
||||||
|
.map { it.length() }.sum()
|
||||||
|
MemoryUtil.bytesToSizeUnit(bytes.toFloat())
|
||||||
|
} else {
|
||||||
|
MemoryUtil.bytesToSizeUnit(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
shaderCacheDir.deleteRecursively()
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
R.string.cleared_shaders_successfully,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
reloadList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.listProperties.apply {
|
||||||
|
layoutManager =
|
||||||
|
GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
|
||||||
|
adapter = GamePropertiesAdapter(viewLifecycleOwner, properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
driverViewModel.updateDriverNameForGame(args.game)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val smallLayout = resources.getBoolean(R.bool.small_layout)
|
||||||
|
if (smallLayout) {
|
||||||
|
val mlpListAll =
|
||||||
|
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpListAll.leftMargin = leftInsets
|
||||||
|
mlpListAll.rightMargin = rightInsets
|
||||||
|
binding.listAll.layoutParams = mlpListAll
|
||||||
|
} else {
|
||||||
|
if (ViewCompat.getLayoutDirection(binding.root) ==
|
||||||
|
ViewCompat.LAYOUT_DIRECTION_LTR
|
||||||
|
) {
|
||||||
|
val mlpListAll =
|
||||||
|
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpListAll.rightMargin = rightInsets
|
||||||
|
binding.listAll.layoutParams = mlpListAll
|
||||||
|
|
||||||
|
val mlpIconLayout =
|
||||||
|
binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpIconLayout.topMargin = barInsets.top
|
||||||
|
mlpIconLayout.leftMargin = leftInsets
|
||||||
|
binding.iconLayout!!.layoutParams = mlpIconLayout
|
||||||
|
} else {
|
||||||
|
val mlpListAll =
|
||||||
|
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpListAll.leftMargin = leftInsets
|
||||||
|
binding.listAll.layoutParams = mlpListAll
|
||||||
|
|
||||||
|
val mlpIconLayout =
|
||||||
|
binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpIconLayout.topMargin = barInsets.top
|
||||||
|
mlpIconLayout.rightMargin = rightInsets
|
||||||
|
binding.iconLayout!!.layoutParams = mlpIconLayout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||||
|
val mlpFab =
|
||||||
|
binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpFab.leftMargin = leftInsets + fabSpacing
|
||||||
|
mlpFab.rightMargin = rightInsets + fabSpacing
|
||||||
|
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
|
||||||
|
binding.buttonStart.layoutParams = mlpFab
|
||||||
|
|
||||||
|
binding.layoutAll.updatePadding(
|
||||||
|
top = barInsets.top,
|
||||||
|
bottom = barInsets.bottom +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||||
|
)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
private val importSaves =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val inputZip = requireContext().contentResolver.openInputStream(result)
|
||||||
|
val savesFolder = File(args.game.saveDir)
|
||||||
|
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
||||||
|
cacheSaveDir.mkdir()
|
||||||
|
|
||||||
|
if (inputZip == null) {
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
getString(R.string.fatal_error),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
R.string.save_files_importing,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
||||||
|
val files = cacheSaveDir.listFiles()
|
||||||
|
var savesFolderFile: File? = null
|
||||||
|
if (files != null) {
|
||||||
|
val savesFolderName = args.game.programIdHex
|
||||||
|
for (file in files) {
|
||||||
|
if (file.isDirectory && file.name == savesFolderName) {
|
||||||
|
savesFolderFile = file
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savesFolderFile != null) {
|
||||||
|
savesFolder.deleteRecursively()
|
||||||
|
savesFolder.mkdir()
|
||||||
|
savesFolderFile.copyRecursively(savesFolder)
|
||||||
|
savesFolderFile.deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (savesFolderFile == null) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.save_file_invalid_zip_structure,
|
||||||
|
descriptionId = R.string.save_file_invalid_zip_structure_description
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
getString(R.string.save_file_imported_success),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
reloadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheSaveDir.deleteRecursively()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
getString(R.string.fatal_error),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the save file located in the given folder path by creating a zip file and opening a
|
||||||
|
* file picker to save.
|
||||||
|
*/
|
||||||
|
private val exportSaves = registerForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/zip")
|
||||||
|
) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
R.string.save_files_exporting,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
val saveLocation = args.game.saveDir
|
||||||
|
val zipResult = FileUtil.zipFromInternalStorage(
|
||||||
|
File(saveLocation),
|
||||||
|
saveLocation.replaceAfterLast("/", ""),
|
||||||
|
BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
|
||||||
|
)
|
||||||
|
return@newInstance when (zipResult) {
|
||||||
|
TaskState.Completed -> getString(R.string.export_success)
|
||||||
|
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
||||||
|
}
|
||||||
|
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
}
|
|
@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
titleId: Int,
|
titleId: Int,
|
||||||
cancellable: Boolean = false,
|
cancellable: Boolean = false,
|
||||||
task: () -> Any
|
task: suspend () -> Any
|
||||||
): IndeterminateProgressDialogFragment {
|
): IndeterminateProgressDialogFragment {
|
||||||
val dialog = IndeterminateProgressDialogFragment()
|
val dialog = IndeterminateProgressDialogFragment()
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
|
||||||
|
|
||||||
|
class LaunchGameDialogFragment : DialogFragment() {
|
||||||
|
private var selectedItem = 0
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val game = requireArguments().parcelable<Game>(GAME)
|
||||||
|
val launchOptions = arrayOf(getString(R.string.global), getString(R.string.custom))
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.launch_options)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
val action = HomeNavigationDirections
|
||||||
|
.actionGlobalEmulationActivity(game, selectedItem != 0)
|
||||||
|
requireParentFragment().findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
.setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
|
||||||
|
selectedItem = i
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(SELECTED_ITEM, selectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "LaunchGameDialogFragment"
|
||||||
|
|
||||||
|
const val GAME = "Game"
|
||||||
|
const val SELECTED_ITEM = "SelectedItem"
|
||||||
|
|
||||||
|
fun newInstance(game: Game): LaunchGameDialogFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putParcelable(GAME, game)
|
||||||
|
val fragment = LaunchGameDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,30 +27,31 @@ class MessageDialogFragment : DialogFragment() {
|
||||||
val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
|
val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
|
||||||
val helpLinkId = requireArguments().getInt(HELP_LINK)
|
val helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
.setPositiveButton(R.string.close, null)
|
|
||||||
|
|
||||||
if (titleId != 0) dialog.setTitle(titleId)
|
if (messageDialogViewModel.positiveAction == null) {
|
||||||
if (titleString.isNotEmpty()) dialog.setTitle(titleString)
|
builder.setPositiveButton(R.string.close, null)
|
||||||
|
} else {
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
messageDialogViewModel.positiveAction?.invoke()
|
||||||
|
}.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleId != 0) builder.setTitle(titleId)
|
||||||
|
if (titleString.isNotEmpty()) builder.setTitle(titleString)
|
||||||
|
|
||||||
if (descriptionId != 0) {
|
if (descriptionId != 0) {
|
||||||
dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
|
builder.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
|
||||||
}
|
}
|
||||||
if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString)
|
if (descriptionString.isNotEmpty()) builder.setMessage(descriptionString)
|
||||||
|
|
||||||
if (helpLinkId != 0) {
|
if (helpLinkId != 0) {
|
||||||
dialog.setNeutralButton(R.string.learn_more) { _, _ ->
|
builder.setNeutralButton(R.string.learn_more) { _, _ ->
|
||||||
openLink(getString(helpLinkId))
|
openLink(getString(helpLinkId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialog.show()
|
return builder.show()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
messageDialogViewModel.dismissAction.invoke()
|
|
||||||
messageDialogViewModel.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openLink(link: String) {
|
private fun openLink(link: String) {
|
||||||
|
@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() {
|
||||||
descriptionId: Int = 0,
|
descriptionId: Int = 0,
|
||||||
descriptionString: String = "",
|
descriptionString: String = "",
|
||||||
helpLinkId: Int = 0,
|
helpLinkId: Int = 0,
|
||||||
dismissAction: () -> Unit = {}
|
positiveAction: (() -> Unit)? = null
|
||||||
): MessageDialogFragment {
|
): MessageDialogFragment {
|
||||||
val dialog = MessageDialogFragment()
|
val dialog = MessageDialogFragment()
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
|
@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() {
|
||||||
putString(DESCRIPTION_STRING, descriptionString)
|
putString(DESCRIPTION_STRING, descriptionString)
|
||||||
putInt(HELP_LINK, helpLinkId)
|
putInt(HELP_LINK, helpLinkId)
|
||||||
}
|
}
|
||||||
ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
|
ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
|
||||||
dismissAction
|
clear()
|
||||||
|
this.positiveAction = positiveAction
|
||||||
|
}
|
||||||
dialog.arguments = bundle
|
dialog.arguments = bundle
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,9 @@ class SearchFragment : Fragment() {
|
||||||
// This is using the correct scope, lint is just acting up
|
// This is using the correct scope, lint is just acting up
|
||||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
homeViewModel.setNavigationVisibility(visible = true, animated = false)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(true)
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
data class Addon(
|
||||||
|
var enabled: Boolean,
|
||||||
|
val title: String,
|
||||||
|
val version: String
|
||||||
|
)
|
|
@ -0,0 +1,83 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
class AddonViewModel : ViewModel() {
|
||||||
|
private val _addonList = MutableStateFlow(mutableListOf<Addon>())
|
||||||
|
val addonList get() = _addonList.asStateFlow()
|
||||||
|
|
||||||
|
private val _showModInstallPicker = MutableStateFlow(false)
|
||||||
|
val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
|
||||||
|
|
||||||
|
private val _showModNoticeDialog = MutableStateFlow(false)
|
||||||
|
val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
|
||||||
|
|
||||||
|
var game: Game? = null
|
||||||
|
|
||||||
|
private val isRefreshing = AtomicBoolean(false)
|
||||||
|
|
||||||
|
fun onOpenAddons(game: Game) {
|
||||||
|
this.game = game
|
||||||
|
refreshAddons()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAddons() {
|
||||||
|
if (isRefreshing.get() || game == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isRefreshing.set(true)
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val addonList = mutableListOf<Addon>()
|
||||||
|
val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId)
|
||||||
|
NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach {
|
||||||
|
val name = it.first.replace("[D] ", "")
|
||||||
|
addonList.add(Addon(!disabledAddons.contains(name), name, it.second))
|
||||||
|
}
|
||||||
|
addonList.sortBy { it.title }
|
||||||
|
_addonList.value = addonList
|
||||||
|
isRefreshing.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCloseAddons() {
|
||||||
|
if (_addonList.value.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeConfig.setDisabledAddons(
|
||||||
|
game!!.programId,
|
||||||
|
_addonList.value.mapNotNull {
|
||||||
|
if (it.enabled) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
it.title
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
)
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
_addonList.value.clear()
|
||||||
|
game = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showModInstallPicker(install: Boolean) {
|
||||||
|
_showModInstallPicker.value = install
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showModNoticeDialog(show: Boolean) {
|
||||||
|
_showModNoticeDialog.value = show
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,10 +3,18 @@
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.model
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import java.util.HashSet
|
import java.util.HashSet
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -15,12 +23,44 @@ class Game(
|
||||||
val path: String,
|
val path: String,
|
||||||
val programId: String = "",
|
val programId: String = "",
|
||||||
val developer: String = "",
|
val developer: String = "",
|
||||||
val version: String = "",
|
var version: String = "",
|
||||||
val isHomebrew: Boolean = false
|
val isHomebrew: Boolean = false
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
|
val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
|
||||||
val keyLastPlayedTime get() = "${path}_LastPlayed"
|
val keyLastPlayedTime get() = "${path}_LastPlayed"
|
||||||
|
|
||||||
|
val settingsName: String
|
||||||
|
get() {
|
||||||
|
val programIdLong = programId.toLong()
|
||||||
|
return if (programIdLong == 0L) {
|
||||||
|
FileUtil.getFilename(Uri.parse(path))
|
||||||
|
} else {
|
||||||
|
"0" + programIdLong.toString(16).uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val programIdHex: String
|
||||||
|
get() {
|
||||||
|
val programIdLong = programId.toLong()
|
||||||
|
return if (programIdLong == 0L) {
|
||||||
|
"0"
|
||||||
|
} else {
|
||||||
|
"0" + programIdLong.toString(16).uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveZipName: String
|
||||||
|
get() = "$title ${YuzuApplication.appContext.getString(R.string.save_data).lowercase()} - ${
|
||||||
|
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
||||||
|
}.zip"
|
||||||
|
|
||||||
|
val saveDir: String
|
||||||
|
get() = DirectoryInitialization.userDirectory + "/nand" +
|
||||||
|
NativeLibrary.getSavePath(programId)
|
||||||
|
|
||||||
|
val addonDir: String
|
||||||
|
get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/"
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other !is Game) {
|
if (other !is Game) {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface GameProperty {
|
||||||
|
@get:StringRes
|
||||||
|
val titleId: Int
|
||||||
|
get() = -1
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val descriptionId: Int
|
||||||
|
get() = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SubmenuProperty(
|
||||||
|
override val titleId: Int,
|
||||||
|
override val descriptionId: Int,
|
||||||
|
@DrawableRes val iconId: Int,
|
||||||
|
val details: (() -> String)? = null,
|
||||||
|
val detailsFlow: StateFlow<String>? = null,
|
||||||
|
val action: () -> Unit
|
||||||
|
) : GameProperty
|
||||||
|
|
||||||
|
data class InstallableProperty(
|
||||||
|
override val titleId: Int,
|
||||||
|
override val descriptionId: Int,
|
||||||
|
val install: (() -> Unit)? = null,
|
||||||
|
val export: (() -> Unit)? = null
|
||||||
|
) : GameProperty
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.model
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
@ -21,6 +22,12 @@ class HomeViewModel : ViewModel() {
|
||||||
private val _gamesDirSelected = MutableStateFlow(false)
|
private val _gamesDirSelected = MutableStateFlow(false)
|
||||||
val gamesDirSelected get() = _gamesDirSelected.asStateFlow()
|
val gamesDirSelected get() = _gamesDirSelected.asStateFlow()
|
||||||
|
|
||||||
|
private val _openImportSaves = MutableStateFlow(false)
|
||||||
|
val openImportSaves get() = _openImportSaves.asStateFlow()
|
||||||
|
|
||||||
|
private val _contentToInstall = MutableStateFlow<List<Uri>?>(null)
|
||||||
|
val contentToInstall get() = _contentToInstall.asStateFlow()
|
||||||
|
|
||||||
var navigatedToSetup = false
|
var navigatedToSetup = false
|
||||||
|
|
||||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||||
|
@ -44,4 +51,12 @@ class HomeViewModel : ViewModel() {
|
||||||
fun setGamesDirSelected(selected: Boolean) {
|
fun setGamesDirSelected(selected: Boolean) {
|
||||||
_gamesDirSelected.value = selected
|
_gamesDirSelected.value = selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setOpenImportSaves(import: Boolean) {
|
||||||
|
_openImportSaves.value = import
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentToInstall(documents: List<Uri>?) {
|
||||||
|
_contentToInstall.value = documents
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ package org.yuzu.yuzu_emu.model
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
class MessageDialogViewModel : ViewModel() {
|
class MessageDialogViewModel : ViewModel() {
|
||||||
var dismissAction: () -> Unit = {}
|
var positiveAction: (() -> Unit)? = null
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
dismissAction = {}
|
positiveAction = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() {
|
||||||
val cancelled: StateFlow<Boolean> get() = _cancelled
|
val cancelled: StateFlow<Boolean> get() = _cancelled
|
||||||
private val _cancelled = MutableStateFlow(false)
|
private val _cancelled = MutableStateFlow(false)
|
||||||
|
|
||||||
lateinit var task: () -> Any
|
lateinit var task: suspend () -> Any
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
_result.value = Any()
|
_result.value = Any()
|
||||||
|
|
|
@ -19,7 +19,7 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.google.android.material.transition.MaterialFadeThrough
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||||
|
@ -35,11 +35,6 @@ class GamesFragment : Fragment() {
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enterTransition = MaterialFadeThrough()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
@ -52,7 +47,9 @@ class GamesFragment : Fragment() {
|
||||||
// This is using the correct scope, lint is just acting up
|
// This is using the correct scope, lint is just acting up
|
||||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
homeViewModel.setNavigationVisibility(visible = true, animated = false)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(true)
|
||||||
|
|
||||||
binding.gridGames.apply {
|
binding.gridGames.apply {
|
||||||
layoutManager = AutofitGridLayoutManager(
|
layoutManager = AutofitGridLayoutManager(
|
||||||
|
|
|
@ -43,7 +43,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||||
import org.yuzu.yuzu_emu.getPublicFilesDir
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
import org.yuzu.yuzu_emu.model.TaskState
|
import org.yuzu.yuzu_emu.model.TaskState
|
||||||
|
@ -60,15 +60,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
private val homeViewModel: HomeViewModel by viewModels()
|
private val homeViewModel: HomeViewModel by viewModels()
|
||||||
private val gamesViewModel: GamesViewModel by viewModels()
|
private val gamesViewModel: GamesViewModel by viewModels()
|
||||||
private val taskViewModel: TaskViewModel by viewModels()
|
private val taskViewModel: TaskViewModel by viewModels()
|
||||||
|
private val addonViewModel: AddonViewModel by viewModels()
|
||||||
|
|
||||||
override var themeId: Int = 0
|
override var themeId: Int = 0
|
||||||
|
|
||||||
private val savesFolder
|
|
||||||
get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
|
|
||||||
|
|
||||||
// Get first subfolder in saves folder (should be the user folder)
|
|
||||||
val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
||||||
|
@ -145,6 +140,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
|
homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.contentToInstall.collect {
|
||||||
|
if (it != null) {
|
||||||
|
installContent(it)
|
||||||
|
homeViewModel.setContentToInstall(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dismiss previous notifications (should not happen unless a crash occurred)
|
// Dismiss previous notifications (should not happen unless a crash occurred)
|
||||||
|
@ -468,110 +473,150 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
val installGameUpdate = registerForActivityResult(
|
val installGameUpdate = registerForActivityResult(
|
||||||
ActivityResultContracts.OpenMultipleDocuments()
|
ActivityResultContracts.OpenMultipleDocuments()
|
||||||
) { documents: List<Uri> ->
|
) { documents: List<Uri> ->
|
||||||
if (documents.isNotEmpty()) {
|
if (documents.isEmpty()) {
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
return@registerForActivityResult
|
||||||
this@MainActivity,
|
|
||||||
R.string.installing_game_content
|
|
||||||
) {
|
|
||||||
var installSuccess = 0
|
|
||||||
var installOverwrite = 0
|
|
||||||
var errorBaseGame = 0
|
|
||||||
var errorExtension = 0
|
|
||||||
var errorOther = 0
|
|
||||||
documents.forEach {
|
|
||||||
when (
|
|
||||||
NativeLibrary.installFileToNand(
|
|
||||||
it.toString(),
|
|
||||||
FileUtil.getExtension(it)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
NativeLibrary.InstallFileToNandResult.Success -> {
|
|
||||||
installSuccess += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
|
|
||||||
installOverwrite += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
|
|
||||||
errorBaseGame += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
|
|
||||||
errorExtension += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
errorOther += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val separator = System.getProperty("line.separator") ?: "\n"
|
|
||||||
val installResult = StringBuilder()
|
|
||||||
if (installSuccess > 0) {
|
|
||||||
installResult.append(
|
|
||||||
getString(
|
|
||||||
R.string.install_game_content_success_install,
|
|
||||||
installSuccess
|
|
||||||
)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
if (installOverwrite > 0) {
|
|
||||||
installResult.append(
|
|
||||||
getString(
|
|
||||||
R.string.install_game_content_success_overwrite,
|
|
||||||
installOverwrite
|
|
||||||
)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
val errorTotal: Int = errorBaseGame + errorExtension + errorOther
|
|
||||||
if (errorTotal > 0) {
|
|
||||||
installResult.append(separator)
|
|
||||||
installResult.append(
|
|
||||||
getString(
|
|
||||||
R.string.install_game_content_failed_count,
|
|
||||||
errorTotal
|
|
||||||
)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
if (errorBaseGame > 0) {
|
|
||||||
installResult.append(separator)
|
|
||||||
installResult.append(
|
|
||||||
getString(R.string.install_game_content_failure_base)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
if (errorExtension > 0) {
|
|
||||||
installResult.append(separator)
|
|
||||||
installResult.append(
|
|
||||||
getString(R.string.install_game_content_failure_file_extension)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
if (errorOther > 0) {
|
|
||||||
installResult.append(
|
|
||||||
getString(R.string.install_game_content_failure_description)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.install_game_content_failure,
|
|
||||||
descriptionString = installResult.toString().trim(),
|
|
||||||
helpLinkId = R.string.install_game_content_help_link
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.install_game_content_success,
|
|
||||||
descriptionString = installResult.toString().trim()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (addonViewModel.game == null) {
|
||||||
|
installContent(documents)
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
this@MainActivity,
|
||||||
|
R.string.verifying_content,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
var updatesMatchProgram = true
|
||||||
|
for (document in documents) {
|
||||||
|
val valid = NativeLibrary.doesUpdateMatchProgram(
|
||||||
|
addonViewModel.game!!.programId,
|
||||||
|
document.toString()
|
||||||
|
)
|
||||||
|
if (!valid) {
|
||||||
|
updatesMatchProgram = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatesMatchProgram) {
|
||||||
|
homeViewModel.setContentToInstall(documents)
|
||||||
|
} else {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
this@MainActivity,
|
||||||
|
titleId = R.string.content_install_notice,
|
||||||
|
descriptionId = R.string.content_install_notice_description,
|
||||||
|
positiveAction = { homeViewModel.setContentToInstall(documents) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installContent(documents: List<Uri>) {
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
this@MainActivity,
|
||||||
|
R.string.installing_game_content
|
||||||
|
) {
|
||||||
|
var installSuccess = 0
|
||||||
|
var installOverwrite = 0
|
||||||
|
var errorBaseGame = 0
|
||||||
|
var errorExtension = 0
|
||||||
|
var errorOther = 0
|
||||||
|
documents.forEach {
|
||||||
|
when (
|
||||||
|
NativeLibrary.installFileToNand(
|
||||||
|
it.toString(),
|
||||||
|
FileUtil.getExtension(it)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
NativeLibrary.InstallFileToNandResult.Success -> {
|
||||||
|
installSuccess += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
|
||||||
|
installOverwrite += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
|
||||||
|
errorBaseGame += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
|
||||||
|
errorExtension += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
errorOther += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addonViewModel.refreshAddons()
|
||||||
|
|
||||||
|
val separator = System.getProperty("line.separator") ?: "\n"
|
||||||
|
val installResult = StringBuilder()
|
||||||
|
if (installSuccess > 0) {
|
||||||
|
installResult.append(
|
||||||
|
getString(
|
||||||
|
R.string.install_game_content_success_install,
|
||||||
|
installSuccess
|
||||||
|
)
|
||||||
|
)
|
||||||
|
installResult.append(separator)
|
||||||
|
}
|
||||||
|
if (installOverwrite > 0) {
|
||||||
|
installResult.append(
|
||||||
|
getString(
|
||||||
|
R.string.install_game_content_success_overwrite,
|
||||||
|
installOverwrite
|
||||||
|
)
|
||||||
|
)
|
||||||
|
installResult.append(separator)
|
||||||
|
}
|
||||||
|
val errorTotal: Int = errorBaseGame + errorExtension + errorOther
|
||||||
|
if (errorTotal > 0) {
|
||||||
|
installResult.append(separator)
|
||||||
|
installResult.append(
|
||||||
|
getString(
|
||||||
|
R.string.install_game_content_failed_count,
|
||||||
|
errorTotal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
installResult.append(separator)
|
||||||
|
if (errorBaseGame > 0) {
|
||||||
|
installResult.append(separator)
|
||||||
|
installResult.append(
|
||||||
|
getString(R.string.install_game_content_failure_base)
|
||||||
|
)
|
||||||
|
installResult.append(separator)
|
||||||
|
}
|
||||||
|
if (errorExtension > 0) {
|
||||||
|
installResult.append(separator)
|
||||||
|
installResult.append(
|
||||||
|
getString(R.string.install_game_content_failure_file_extension)
|
||||||
|
)
|
||||||
|
installResult.append(separator)
|
||||||
|
}
|
||||||
|
if (errorOther > 0) {
|
||||||
|
installResult.append(
|
||||||
|
getString(R.string.install_game_content_failure_description)
|
||||||
|
)
|
||||||
|
installResult.append(separator)
|
||||||
|
}
|
||||||
|
return@newInstance MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
|
titleId = R.string.install_game_content_failure,
|
||||||
|
descriptionString = installResult.toString().trim(),
|
||||||
|
helpLinkId = R.string.install_game_content_help_link
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return@newInstance MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
|
titleId = R.string.install_game_content_success,
|
||||||
|
descriptionString = installResult.toString().trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
val exportUserData = registerForActivityResult(
|
val exportUserData = registerForActivityResult(
|
||||||
|
@ -657,102 +702,4 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
return@newInstance getString(R.string.user_data_import_success)
|
return@newInstance getString(R.string.user_data_import_success)
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
|
|
||||||
*/
|
|
||||||
val exportSaves = registerForActivityResult(
|
|
||||||
ActivityResultContracts.CreateDocument("application/zip")
|
|
||||||
) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
R.string.save_files_exporting,
|
|
||||||
false
|
|
||||||
) {
|
|
||||||
val zipResult = FileUtil.zipFromInternalStorage(
|
|
||||||
File(savesFolderRoot),
|
|
||||||
savesFolderRoot,
|
|
||||||
BufferedOutputStream(contentResolver.openOutputStream(result))
|
|
||||||
)
|
|
||||||
return@newInstance when (zipResult) {
|
|
||||||
TaskState.Completed -> getString(R.string.export_success)
|
|
||||||
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
|
||||||
}
|
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val startForResultExportSave =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
|
|
||||||
File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
val importSaves =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.initializeEmptyUserDirectory()
|
|
||||||
|
|
||||||
val inputZip = contentResolver.openInputStream(result)
|
|
||||||
// A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
|
|
||||||
var validZip = false
|
|
||||||
val savesFolder = File(savesFolderRoot)
|
|
||||||
val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/")
|
|
||||||
cacheSaveDir.mkdir()
|
|
||||||
|
|
||||||
if (inputZip == null) {
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
getString(R.string.fatal_error),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val filterTitleId =
|
|
||||||
FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
|
||||||
cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
|
|
||||||
File(savesFolder, savePath).deleteRecursively()
|
|
||||||
File(cacheSaveDir, savePath).copyRecursively(
|
|
||||||
File(savesFolder, savePath),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
validZip = true
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (!validZip) {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
this@MainActivity,
|
|
||||||
titleId = R.string.save_file_invalid_zip_structure,
|
|
||||||
descriptionId = R.string.save_file_invalid_zip_structure_description
|
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
getString(R.string.save_file_imported_success),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheSaveDir.deleteRecursively()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
getString(R.string.fatal_error),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
object AddonUtil {
|
||||||
|
val validAddonDirectories = listOf("cheats", "exefs", "romfs")
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import java.io.BufferedOutputStream
|
||||||
import java.lang.NullPointerException
|
import java.lang.NullPointerException
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
import kotlin.IllegalStateException
|
||||||
|
|
||||||
object FileUtil {
|
object FileUtil {
|
||||||
const val PATH_TREE = "tree"
|
const val PATH_TREE = "tree"
|
||||||
|
@ -342,6 +343,37 @@ object FileUtil {
|
||||||
return TaskState.Completed
|
return TaskState.Completed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that copies the contents of a DocumentFile folder into a [File]
|
||||||
|
* @param file [File] representation of the folder to copy into
|
||||||
|
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
|
||||||
|
*/
|
||||||
|
fun DocumentFile.copyFilesTo(file: File) {
|
||||||
|
file.mkdirs()
|
||||||
|
if (!this.isDirectory || !file.isDirectory) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"[FileUtil] Tried to copy a folder into a file or vice versa"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listFiles().forEach {
|
||||||
|
val newFile = File(file, it.name!!)
|
||||||
|
if (it.isDirectory) {
|
||||||
|
newFile.mkdirs()
|
||||||
|
DocumentFile.fromTreeUri(YuzuApplication.appContext, it.uri)?.copyFilesTo(newFile)
|
||||||
|
} else {
|
||||||
|
val inputStream =
|
||||||
|
YuzuApplication.appContext.contentResolver.openInputStream(it.uri)
|
||||||
|
BufferedInputStream(inputStream).use { bos ->
|
||||||
|
if (!newFile.exists()) {
|
||||||
|
newFile.createNewFile()
|
||||||
|
}
|
||||||
|
newFile.outputStream().use { os -> bos.copyTo(os) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun isRootTreeUri(uri: Uri): Boolean {
|
fun isRootTreeUri(uri: Uri): Boolean {
|
||||||
val paths = uri.pathSegments
|
val paths = uri.pathSegments
|
||||||
return paths.size == 2 && PATH_TREE == paths[0]
|
return paths.size == 2 && PATH_TREE == paths[0]
|
||||||
|
|
|
@ -105,4 +105,23 @@ object NativeConfig {
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun addGameDir(dir: GameDir)
|
external fun addGameDir(dir: GameDir)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an array of the addons that are disabled for a given game
|
||||||
|
*
|
||||||
|
* @param programId String representation of a game's program ID
|
||||||
|
* @return An array of disabled addons
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun getDisabledAddons(programId: String): Array<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the disabled addons array corresponding to [programId] and replaces them
|
||||||
|
* with [disabledAddons]
|
||||||
|
*
|
||||||
|
* @param programId String representation of a game's program ID
|
||||||
|
* @param disabledAddons Replacement array of disabled addons
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun setDisabledAddons(programId: String, disabledAddons: Array<String>)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,12 @@ static jmethodID s_disk_cache_load_progress;
|
||||||
static jmethodID s_on_emulation_started;
|
static jmethodID s_on_emulation_started;
|
||||||
static jmethodID s_on_emulation_stopped;
|
static jmethodID s_on_emulation_stopped;
|
||||||
|
|
||||||
|
static jclass s_string_class;
|
||||||
|
static jclass s_pair_class;
|
||||||
|
static jmethodID s_pair_constructor;
|
||||||
|
static jfieldID s_pair_first_field;
|
||||||
|
static jfieldID s_pair_second_field;
|
||||||
|
|
||||||
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||||
|
|
||||||
namespace IDCache {
|
namespace IDCache {
|
||||||
|
@ -79,6 +85,26 @@ jmethodID GetOnEmulationStopped() {
|
||||||
return s_on_emulation_stopped;
|
return s_on_emulation_stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetStringClass() {
|
||||||
|
return s_string_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jclass GetPairClass() {
|
||||||
|
return s_pair_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetPairConstructor() {
|
||||||
|
return s_pair_constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
jfieldID GetPairFirstField() {
|
||||||
|
return s_pair_first_field;
|
||||||
|
}
|
||||||
|
|
||||||
|
jfieldID GetPairSecondField() {
|
||||||
|
return s_pair_second_field;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -115,6 +141,18 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
s_on_emulation_stopped =
|
s_on_emulation_stopped =
|
||||||
env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
|
env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
|
||||||
|
|
||||||
|
const jclass string_class = env->FindClass("java/lang/String");
|
||||||
|
s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class));
|
||||||
|
env->DeleteLocalRef(string_class);
|
||||||
|
|
||||||
|
const jclass pair_class = env->FindClass("kotlin/Pair");
|
||||||
|
s_pair_class = reinterpret_cast<jclass>(env->NewGlobalRef(pair_class));
|
||||||
|
s_pair_constructor =
|
||||||
|
env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V");
|
||||||
|
s_pair_first_field = env->GetFieldID(pair_class, "first", "Ljava/lang/Object;");
|
||||||
|
s_pair_second_field = env->GetFieldID(pair_class, "second", "Ljava/lang/Object;");
|
||||||
|
env->DeleteLocalRef(pair_class);
|
||||||
|
|
||||||
// Initialize Android Storage
|
// Initialize Android Storage
|
||||||
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
||||||
|
|
||||||
|
@ -136,6 +174,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||||
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
||||||
env->DeleteGlobalRef(s_load_callback_stage_class);
|
env->DeleteGlobalRef(s_load_callback_stage_class);
|
||||||
env->DeleteGlobalRef(s_game_dir_class);
|
env->DeleteGlobalRef(s_game_dir_class);
|
||||||
|
env->DeleteGlobalRef(s_string_class);
|
||||||
|
env->DeleteGlobalRef(s_pair_class);
|
||||||
|
|
||||||
// UnInitialize applets
|
// UnInitialize applets
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
|
|
|
@ -20,4 +20,10 @@ jmethodID GetDiskCacheLoadProgress();
|
||||||
jmethodID GetOnEmulationStarted();
|
jmethodID GetOnEmulationStarted();
|
||||||
jmethodID GetOnEmulationStopped();
|
jmethodID GetOnEmulationStopped();
|
||||||
|
|
||||||
|
jclass GetStringClass();
|
||||||
|
jclass GetPairClass();
|
||||||
|
jmethodID GetPairConstructor();
|
||||||
|
jfieldID GetPairFirstField();
|
||||||
|
jfieldID GetPairSecondField();
|
||||||
|
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#include <android/api-level.h>
|
#include <android/api-level.h>
|
||||||
#include <android/native_window_jni.h>
|
#include <android/native_window_jni.h>
|
||||||
#include <common/fs/fs.h>
|
#include <common/fs/fs.h>
|
||||||
|
#include <core/file_sys/patch_manager.h>
|
||||||
#include <core/file_sys/savedata_factory.h>
|
#include <core/file_sys/savedata_factory.h>
|
||||||
#include <core/loader/nro.h>
|
#include <core/loader/nro.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
@ -79,6 +80,10 @@ Core::System& EmulationSession::System() {
|
||||||
return m_system;
|
return m_system;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileSys::ManualContentProvider* EmulationSession::ContentProvider() {
|
||||||
|
return m_manual_provider.get();
|
||||||
|
}
|
||||||
|
|
||||||
const EmuWindow_Android& EmulationSession::Window() const {
|
const EmuWindow_Android& EmulationSession::Window() const {
|
||||||
return *m_window;
|
return *m_window;
|
||||||
}
|
}
|
||||||
|
@ -455,6 +460,15 @@ void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
|
||||||
static_cast<jint>(result));
|
static_cast<jint>(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u64 EmulationSession::GetProgramId(JNIEnv* env, jstring jprogramId) {
|
||||||
|
auto program_id_string = GetJString(env, jprogramId);
|
||||||
|
try {
|
||||||
|
return std::stoull(program_id_string);
|
||||||
|
} catch (...) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
|
static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
|
||||||
MicroProfileOnThreadCreate("EmuThread");
|
MicroProfileOnThreadCreate("EmuThread");
|
||||||
SCOPE_EXIT({ MicroProfileShutdown(); });
|
SCOPE_EXIT({ MicroProfileShutdown(); });
|
||||||
|
@ -504,6 +518,27 @@ int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject
|
||||||
GetJString(env, j_file_extension));
|
GetJString(env, j_file_extension));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
|
||||||
|
jstring jprogramId,
|
||||||
|
jstring jupdatePath) {
|
||||||
|
u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||||
|
std::string updatePath = GetJString(env, jupdatePath);
|
||||||
|
std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>(
|
||||||
|
EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(updatePath,
|
||||||
|
FileSys::Mode::Read));
|
||||||
|
for (const auto& item : nsp->GetNCAs()) {
|
||||||
|
for (const auto& nca_details : item.second) {
|
||||||
|
if (nca_details.second->GetName().ends_with(".cnmt.nca")) {
|
||||||
|
auto update_id = nca_details.second->GetTitleId() & ~0xFFFULL;
|
||||||
|
if (update_id == program_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz,
|
void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz,
|
||||||
jstring hook_lib_dir,
|
jstring hook_lib_dir,
|
||||||
jstring custom_driver_dir,
|
jstring custom_driver_dir,
|
||||||
|
@ -665,13 +700,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass
|
||||||
EmulationSession::GetInstance().InitializeSystem(reload);
|
EmulationSession::GetInstance().InitializeSystem(reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass clazz) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z(
|
|
||||||
JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate) {}
|
|
||||||
|
|
||||||
jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) {
|
jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) {
|
||||||
jdoubleArray j_stats = env->NewDoubleArray(4);
|
jdoubleArray j_stats = env->NewDoubleArray(4);
|
||||||
|
|
||||||
|
@ -696,9 +724,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass
|
||||||
return ToJString(env, "JIT");
|
return ToJString(env, "JIT");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env,
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
|
||||||
jclass clazz,
|
EmulationSession::GetInstance().System().ApplySettings();
|
||||||
jstring j_path) {}
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {
|
||||||
|
Settings::LogSettings();
|
||||||
|
}
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz,
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz,
|
||||||
jstring j_path) {
|
jstring j_path) {
|
||||||
|
@ -792,4 +824,60 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj,
|
||||||
|
jstring jpath,
|
||||||
|
jstring jprogramId) {
|
||||||
|
const auto path = GetJString(env, jpath);
|
||||||
|
const auto vFile =
|
||||||
|
Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
|
||||||
|
if (vFile == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& system = EmulationSession::GetInstance().System();
|
||||||
|
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||||
|
const FileSys::PatchManager pm{program_id, system.GetFileSystemController(),
|
||||||
|
system.GetContentProvider()};
|
||||||
|
const auto loader = Loader::GetLoader(system, vFile);
|
||||||
|
|
||||||
|
FileSys::VirtualFile update_raw;
|
||||||
|
loader->ReadUpdateRaw(update_raw);
|
||||||
|
|
||||||
|
auto addons = pm.GetPatchVersionNames(update_raw);
|
||||||
|
auto jemptyString = ToJString(env, "");
|
||||||
|
auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
|
||||||
|
jemptyString, jemptyString);
|
||||||
|
jobjectArray jaddonsArray =
|
||||||
|
env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
|
||||||
|
int i = 0;
|
||||||
|
for (const auto& addon : addons) {
|
||||||
|
jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
|
||||||
|
ToJString(env, addon.first), ToJString(env, addon.second));
|
||||||
|
env->SetObjectArrayElement(jaddonsArray, i, jaddon);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return jaddonsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
|
||||||
|
jstring jprogramId) {
|
||||||
|
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||||
|
|
||||||
|
auto& system = EmulationSession::GetInstance().System();
|
||||||
|
|
||||||
|
Service::Account::ProfileManager manager;
|
||||||
|
// TODO: Pass in a selected user once we get the relevant UI working
|
||||||
|
const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
|
||||||
|
ASSERT(user_id);
|
||||||
|
|
||||||
|
const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
|
||||||
|
auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir),
|
||||||
|
FileSys::Mode::Read);
|
||||||
|
|
||||||
|
const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
|
||||||
|
system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
|
||||||
|
program_id, user_id->AsU128(), 0);
|
||||||
|
return ToJString(env, user_save_data_path);
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|
|
@ -54,6 +54,8 @@ public:
|
||||||
|
|
||||||
static void OnEmulationStarted();
|
static void OnEmulationStarted();
|
||||||
|
|
||||||
|
static u64 GetProgramId(JNIEnv* env, jstring jprogramId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
|
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
|
||||||
static void OnEmulationStopped(Core::SystemResultStatus result);
|
static void OnEmulationStopped(Core::SystemResultStatus result);
|
||||||
|
|
|
@ -283,4 +283,30 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject
|
||||||
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
|
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDisabledAddons(JNIEnv* env, jobject obj,
|
||||||
|
jstring jprogramId) {
|
||||||
|
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||||
|
auto& disabledAddons = Settings::values.disabled_addons[program_id];
|
||||||
|
jobjectArray jdisabledAddonsArray =
|
||||||
|
env->NewObjectArray(disabledAddons.size(), IDCache::GetStringClass(), ToJString(env, ""));
|
||||||
|
for (size_t i = 0; i < disabledAddons.size(); ++i) {
|
||||||
|
env->SetObjectArrayElement(jdisabledAddonsArray, i, ToJString(env, disabledAddons[i]));
|
||||||
|
}
|
||||||
|
return jdisabledAddonsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setDisabledAddons(JNIEnv* env, jobject obj,
|
||||||
|
jstring jprogramId,
|
||||||
|
jobjectArray jdisabledAddons) {
|
||||||
|
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||||
|
Settings::values.disabled_addons[program_id].clear();
|
||||||
|
std::vector<std::string> disabled_addons;
|
||||||
|
const int size = env->GetArrayLength(jdisabledAddons);
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
auto jaddon = static_cast<jstring>(env->GetObjectArrayElement(jdisabledAddons, i));
|
||||||
|
disabled_addons.push_back(GetJString(env, jaddon));
|
||||||
|
}
|
||||||
|
Settings::values.disabled_addons[program_id] = disabled_addons;
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/list_all"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:fadeScrollbars="false"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/icon_layout"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_all"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list_properties"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:listitem="@layout/card_simple_outlined" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/icon_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_back"
|
||||||
|
style="?attr/materialIconButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:icon="@drawable/ic_back"
|
||||||
|
app:iconSize="24dp"
|
||||||
|
app:iconTint="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
style="?attr/materialCardViewElevatedStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:cardCornerRadius="4dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_game_screen"
|
||||||
|
android:layout_width="175dp"
|
||||||
|
android:layout_height="175dp"
|
||||||
|
tools:src="@drawable/default_icon" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
style="@style/TextAppearance.Material3.TitleMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="center"
|
||||||
|
tools:text="deko_basic" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/button_start"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/start"
|
||||||
|
app:icon="@drawable/ic_play"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -11,7 +11,8 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dp"
|
android:paddingVertical="16dp"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_gravity="center">
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:padding="24dp">
|
android:paddingVertical="16dp"
|
||||||
|
android:paddingHorizontal="24dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/icon"
|
android:id="@+id/icon"
|
||||||
|
@ -50,6 +51,23 @@
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
tools:text="@string/applets_description" />
|
tools:text="@string/applets_description" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.LabelMedium"
|
||||||
|
android:id="@+id/details"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:text="/tree/primary:Games" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
47
src/android/app/src/main/res/layout/fragment_addons.xml
Normal file
47
src/android/app/src/main/res/layout/fragment_addons.xml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/coordinator_about"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar_addons"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_addons"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:navigationIcon="@drawable/ic_back" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list_addons"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/appbar_addons" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/button_install"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:text="@string/install"
|
||||||
|
app:icon="@drawable/ic_add"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
125
src/android/app/src/main/res/layout/fragment_game_info.xml
Normal file
125
src/android/app/src/main/res/layout/fragment_game_info.xml
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/coordinator_about"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:navigationIcon="@drawable/ic_back" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/scroll_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/content_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/path"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/path_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:editable="false"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="none"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
tools:text="1.0.0" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/program_id"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/program_id_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:editable="false"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="none"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
tools:text="1.0.0" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/developer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/developer_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:editable="false"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="none"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
tools:text="1.0.0" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/version"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/version_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:editable="false"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="none"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
tools:text="1.0.0" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_copy"
|
||||||
|
style="@style/Widget.Material3.Button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/copy_details" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/list_all"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:fadeScrollbars="false"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_all"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_back"
|
||||||
|
style="?attr/materialIconButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
app:icon="@drawable/ic_back"
|
||||||
|
app:iconSize="24dp"
|
||||||
|
app:iconTint="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
style="?attr/materialCardViewElevatedStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:cardCornerRadius="4dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_game_screen"
|
||||||
|
android:layout_width="175dp"
|
||||||
|
android:layout_height="175dp"
|
||||||
|
tools:src="@drawable/default_icon"/>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
style="@style/TextAppearance.Material3.TitleMedium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="center"
|
||||||
|
tools:text="deko_basic" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list_properties"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:listitem="@layout/card_simple_outlined" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/button_start"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/start"
|
||||||
|
app:icon="@drawable/ic_play"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
57
src/android/app/src/main/res/layout/list_item_addon.xml
Normal file
57
src/android/app/src/main/res/layout/list_item_addon.xml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/addon_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:focusable="true"
|
||||||
|
android:paddingHorizontal="20dp"
|
||||||
|
android:paddingVertical="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/addon_switch"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/addon_switch"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/addon_switch">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
style="@style/TextAppearance.Material3.HeadlineMedium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textSize="17sp"
|
||||||
|
app:lineHeight="28dp"
|
||||||
|
tools:text="1440p Resolution" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/version"
|
||||||
|
style="@style/TextAppearance.Material3.BodySmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/spacing_small"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
tools:text="1.0.0" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/addon_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:nextFocusLeft="@id/addon_container"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/text_container"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -124,5 +124,38 @@
|
||||||
android:id="@+id/gameFoldersFragment"
|
android:id="@+id/gameFoldersFragment"
|
||||||
android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
|
android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
|
||||||
android:label="GameFoldersFragment" />
|
android:label="GameFoldersFragment" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/perGamePropertiesFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.GamePropertiesFragment"
|
||||||
|
android:label="PerGamePropertiesFragment" >
|
||||||
|
<argument
|
||||||
|
android:name="game"
|
||||||
|
app:argType="org.yuzu.yuzu_emu.model.Game" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_perGamePropertiesFragment_to_gameInfoFragment"
|
||||||
|
app:destination="@id/gameInfoFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_perGamePropertiesFragment_to_addonsFragment"
|
||||||
|
app:destination="@id/addonsFragment" />
|
||||||
|
</fragment>
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_global_perGamePropertiesFragment"
|
||||||
|
app:destination="@id/perGamePropertiesFragment" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/gameInfoFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
|
||||||
|
android:label="GameInfoFragment" >
|
||||||
|
<argument
|
||||||
|
android:name="game"
|
||||||
|
app:argType="org.yuzu.yuzu_emu.model.Game" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/addonsFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.AddonsFragment"
|
||||||
|
android:label="AddonsFragment" >
|
||||||
|
<argument
|
||||||
|
android:name="game"
|
||||||
|
app:argType="org.yuzu.yuzu_emu.model.Game" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<dimen name="menu_width">256dp</dimen>
|
<dimen name="menu_width">256dp</dimen>
|
||||||
<dimen name="card_width">165dp</dimen>
|
<dimen name="card_width">165dp</dimen>
|
||||||
<dimen name="icon_inset">24dp</dimen>
|
<dimen name="icon_inset">24dp</dimen>
|
||||||
<dimen name="spacing_bottom_list_fab">76dp</dimen>
|
<dimen name="spacing_bottom_list_fab">96dp</dimen>
|
||||||
<dimen name="spacing_fab">24dp</dimen>
|
<dimen name="spacing_fab">24dp</dimen>
|
||||||
|
|
||||||
<dimen name="dialog_margin">20dp</dimen>
|
<dimen name="dialog_margin">20dp</dimen>
|
||||||
|
|
|
@ -91,7 +91,10 @@
|
||||||
<string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string>
|
<string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string>
|
||||||
<string name="manage_save_data">Manage save data</string>
|
<string name="manage_save_data">Manage save data</string>
|
||||||
<string name="manage_save_data_description">Save data found. Please select an option below.</string>
|
<string name="manage_save_data_description">Save data found. Please select an option below.</string>
|
||||||
|
<string name="import_save_warning">Import save data</string>
|
||||||
|
<string name="import_save_warning_description">This will overwrite all existing save data with the provided file. Are you sure that you want to continue?</string>
|
||||||
<string name="import_export_saves_description">Import or export save files</string>
|
<string name="import_export_saves_description">Import or export save files</string>
|
||||||
|
<string name="save_files_importing">Importing save files…</string>
|
||||||
<string name="save_files_exporting">Exporting save files…</string>
|
<string name="save_files_exporting">Exporting save files…</string>
|
||||||
<string name="save_file_imported_success">Imported successfully</string>
|
<string name="save_file_imported_success">Imported successfully</string>
|
||||||
<string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
|
<string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
|
||||||
|
@ -266,6 +269,11 @@
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
<string name="export_success">Exported successfully</string>
|
<string name="export_success">Exported successfully</string>
|
||||||
|
<string name="start">Start</string>
|
||||||
|
<string name="clear">Clear</string>
|
||||||
|
<string name="global">Global</string>
|
||||||
|
<string name="custom">Custom</string>
|
||||||
|
<string name="notice">Notice</string>
|
||||||
|
|
||||||
<!-- GPU driver installation -->
|
<!-- GPU driver installation -->
|
||||||
<string name="select_gpu_driver">Select GPU driver</string>
|
<string name="select_gpu_driver">Select GPU driver</string>
|
||||||
|
@ -291,6 +299,43 @@
|
||||||
<string name="preferences_debug">Debug</string>
|
<string name="preferences_debug">Debug</string>
|
||||||
<string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
|
<string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
|
||||||
|
|
||||||
|
<!-- Game properties -->
|
||||||
|
<string name="info">Info</string>
|
||||||
|
<string name="info_description">Program ID, developer, version</string>
|
||||||
|
<string name="per_game_settings">Per-game settings</string>
|
||||||
|
<string name="per_game_settings_description">Edit settings specific to this game</string>
|
||||||
|
<string name="launch_options">Launch config</string>
|
||||||
|
<string name="path">Path</string>
|
||||||
|
<string name="program_id">Program ID</string>
|
||||||
|
<string name="developer">Developer</string>
|
||||||
|
<string name="version">Version</string>
|
||||||
|
<string name="copy_details">Copy details</string>
|
||||||
|
<string name="add_ons">Add-ons</string>
|
||||||
|
<string name="add_ons_description">Toggle mods, updates and DLC</string>
|
||||||
|
<string name="clear_shader_cache">Clear shader cache</string>
|
||||||
|
<string name="clear_shader_cache_description">Removes all shaders built while playing this game</string>
|
||||||
|
<string name="cleared_shaders_successfully">Cleared shaders successfully</string>
|
||||||
|
<string name="addons_game">Addons: %1$s</string>
|
||||||
|
<string name="save_data">Save data</string>
|
||||||
|
<string name="save_data_description">Manage save data specific to this game</string>
|
||||||
|
<string name="delete_save_data">Delete save data</string>
|
||||||
|
<string name="delete_save_data_description">Removes all save data specific to this game</string>
|
||||||
|
<string name="delete_save_data_warning_description">This irrecoverably removes all of this game\'s save data. Are you sure you want to continue?</string>
|
||||||
|
<string name="save_data_deleted_successfully">Save data deleted successfully</string>
|
||||||
|
<string name="select_content_type">Content type</string>
|
||||||
|
<string name="updates_and_dlc">Updates and DLC</string>
|
||||||
|
<string name="mods_and_cheats">Mods and cheats</string>
|
||||||
|
<string name="addon_notice">Important addon notice</string>
|
||||||
|
<!-- "cheats/" "romfs/" and "exefs/ should not be translated -->
|
||||||
|
<string name="addon_notice_description">In order to install mods and cheats, you must select a folder that contains a cheats/, romfs/, or exefs/ directory. We can\'t verify if these will be compatible with your game so be careful!</string>
|
||||||
|
<string name="invalid_directory">Invalid directory</string>
|
||||||
|
<!-- "cheats/" "romfs/" and "exefs/ should not be translated -->
|
||||||
|
<string name="invalid_directory_description">Please make sure that the directory you selected contains a cheats/, romfs/, or exefs/ folder and try again.</string>
|
||||||
|
<string name="addon_installed_successfully">Addon installed successfully</string>
|
||||||
|
<string name="verifying_content">Verifying content…</string>
|
||||||
|
<string name="content_install_notice">Content install notice</string>
|
||||||
|
<string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string>
|
||||||
|
|
||||||
<!-- ROM loading errors -->
|
<!-- ROM loading errors -->
|
||||||
<string name="loader_error_encrypted">Your ROM is encrypted</string>
|
<string name="loader_error_encrypted">Your ROM is encrypted</string>
|
||||||
<string name="loader_error_encrypted_roms_description"><![CDATA[Please follow the guides to redump your <a href="https://yuzu-emu.org/help/quickstart/#dumping-physical-titles-game-cards">game cartidges</a> or <a href="https://yuzu-emu.org/help/quickstart/#dumping-digital-titles-eshop">installed titles</a>.]]></string>
|
<string name="loader_error_encrypted_roms_description"><![CDATA[Please follow the guides to redump your <a href="https://yuzu-emu.org/help/quickstart/#dumping-physical-titles-game-cards">game cartidges</a> or <a href="https://yuzu-emu.org/help/quickstart/#dumping-digital-titles-eshop">installed titles</a>.]]></string>
|
||||||
|
|
Loading…
Reference in a new issue