mirror of
https://github.com/yuzu-emu/yuzu-android
synced 2025-01-30 22:01:33 -08:00
Merge PR 12335
This commit is contained in:
parent
11b123ba01
commit
fd0433ef16
@ -230,8 +230,6 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun onTouchReleased(finger_id: Int)
|
||||
|
||||
external fun initGameIni(gameID: String?)
|
||||
|
||||
external fun setAppDirectory(directory: String)
|
||||
|
||||
/**
|
||||
@ -241,6 +239,8 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun installFileToNand(filename: String, extension: String): Int
|
||||
|
||||
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
|
||||
|
||||
external fun initializeGpuDriver(
|
||||
hookLibDir: String?,
|
||||
customDriverDir: String?,
|
||||
@ -252,18 +252,11 @@ object NativeLibrary {
|
||||
|
||||
external fun initializeSystem(reload: Boolean)
|
||||
|
||||
external fun defaultCPUCore(): Int
|
||||
|
||||
/**
|
||||
* Begins emulation.
|
||||
*/
|
||||
external fun run(path: String?)
|
||||
|
||||
/**
|
||||
* Begins emulation from the specified savestate.
|
||||
*/
|
||||
external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
|
||||
|
||||
// Surface Handling
|
||||
external fun surfaceChanged(surf: Surface?)
|
||||
|
||||
@ -304,10 +297,9 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun getCpuBackend(): String
|
||||
|
||||
/**
|
||||
* Notifies the core emulation that the orientation has changed.
|
||||
*/
|
||||
external fun notifyOrientationChange(layout_option: Int, rotation: Int)
|
||||
external fun applySettings()
|
||||
|
||||
external fun logSettings()
|
||||
|
||||
enum class CoreError {
|
||||
ErrorSystemFiles,
|
||||
@ -538,6 +530,35 @@ object NativeLibrary {
|
||||
*/
|
||||
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
|
||||
|
||||
/**
|
||||
* Adds a file to the manual filesystem provider in our EmulationSession instance
|
||||
* @param path Path to the file we're adding. Can be a string representation of a [Uri] or
|
||||
* a normal path
|
||||
*/
|
||||
external fun addFileToFilesystemProvider(path: String)
|
||||
|
||||
/**
|
||||
* Clears all files added to the manual filesystem provider in our EmulationSession instance
|
||||
*/
|
||||
external fun clearFilesystemProvider()
|
||||
|
||||
/**
|
||||
* Button type for use in onTouchEvent
|
||||
*/
|
||||
|
@ -172,7 +172,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
|
||||
if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
|
||||
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
|
||||
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
|
||||
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
|
||||
@ -284,7 +284,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
|
||||
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
|
||||
PictureInPictureParams.Builder {
|
||||
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
|
||||
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) {
|
||||
0 -> Rational(16, 9)
|
||||
1 -> Rational(4, 3)
|
||||
2 -> Rational(21, 9)
|
||||
@ -331,7 +331,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
pictureInPictureActions.add(pauseRemoteAction)
|
||||
}
|
||||
|
||||
if (BooleanSetting.AUDIO_MUTED.boolean) {
|
||||
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
||||
val unmuteIcon = Icon.createWithResource(
|
||||
this@EmulationActivity,
|
||||
R.drawable.ic_pip_unmute
|
||||
@ -376,7 +376,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
val isEmulationActive = emulationViewModel.emulationStarted.value &&
|
||||
!emulationViewModel.isEmulationStopping.value
|
||||
pictureInPictureParamsBuilder.setAutoEnterEnabled(
|
||||
BooleanSetting.PICTURE_IN_PICTURE.boolean && isEmulationActive
|
||||
BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive
|
||||
)
|
||||
}
|
||||
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
|
||||
@ -390,9 +390,13 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
|
||||
}
|
||||
if (intent.action == actionUnmute) {
|
||||
if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
||||
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
||||
BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
||||
}
|
||||
} else if (intent.action == actionMute) {
|
||||
if (!BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(true)
|
||||
if (!BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
||||
BooleanSetting.AUDIO_MUTED.setBoolean(true)
|
||||
}
|
||||
}
|
||||
buildPictureInPictureParams()
|
||||
}
|
||||
@ -423,7 +427,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
// Always resume audio, since there is no UI button
|
||||
if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
||||
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
||||
BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.R
|
||||
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.AppletInfo
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): AppletAdapter.AppletViewHolder {
|
||||
CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.apply { root.setOnClickListener(this@AppletAdapter) }
|
||||
.also { return AppletViewHolder(it) }
|
||||
}
|
||||
@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
|
||||
view.findNavController().navigate(action)
|
||||
}
|
||||
|
||||
inner class AppletViewHolder(val binding: CardAppletOptionBinding) :
|
||||
inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var applet: Applet
|
||||
|
||||
|
@ -42,7 +42,7 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
|
||||
if (driverViewModel.selectedDriver > position) {
|
||||
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
|
||||
}
|
||||
if (GpuDriverHelper.customDriverData == driverData.second) {
|
||||
if (GpuDriverHelper.customDriverSettingData == driverData.second) {
|
||||
driverViewModel.setSelectedDriverIndex(0)
|
||||
}
|
||||
driverViewModel.driversToDelete.add(driverData.first)
|
||||
|
@ -44,19 +44,20 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity) :
|
||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||
View.OnClickListener {
|
||||
View.OnClickListener,
|
||||
View.OnLongClickListener {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||
// Create a new view.
|
||||
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.cardGame.setOnClickListener(this)
|
||||
binding.cardGame.setOnLongClickListener(this)
|
||||
|
||||
// Use that view to create a ViewHolder.
|
||||
return GameViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var game: Game
|
||||
@ -157,7 +165,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
||||
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||
return oldItem.programId == newItem.programId
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
interface AbstractBooleanSetting : AbstractSetting {
|
||||
val boolean: Boolean
|
||||
|
||||
fun getBoolean(needsGlobal: Boolean = false): Boolean
|
||||
fun setBoolean(value: Boolean)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
interface AbstractByteSetting : AbstractSetting {
|
||||
val byte: Byte
|
||||
|
||||
fun getByte(needsGlobal: Boolean = false): Byte
|
||||
fun setByte(value: Byte)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
interface AbstractFloatSetting : AbstractSetting {
|
||||
val float: Float
|
||||
|
||||
fun getFloat(needsGlobal: Boolean = false): Float
|
||||
fun setFloat(value: Float)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
interface AbstractIntSetting : AbstractSetting {
|
||||
val int: Int
|
||||
|
||||
fun getInt(needsGlobal: Boolean = false): Int
|
||||
fun setInt(value: Int)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
interface AbstractLongSetting : AbstractSetting {
|
||||
val long: Long
|
||||
|
||||
fun getLong(needsGlobal: Boolean = false): Long
|
||||
fun setLong(value: Long)
|
||||
}
|
||||
|
@ -7,12 +7,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
interface AbstractSetting {
|
||||
val key: String
|
||||
val category: Settings.Category
|
||||
val defaultValue: Any
|
||||
val androidDefault: Any?
|
||||
get() = null
|
||||
val valueAsString: String
|
||||
get() = ""
|
||||
|
||||
val isRuntimeModifiable: Boolean
|
||||
get() = NativeConfig.getIsRuntimeModifiable(key)
|
||||
@ -20,5 +15,14 @@ interface AbstractSetting {
|
||||
val pairedSettingKey: String
|
||||
get() = NativeConfig.getPairedSettingKey(key)
|
||||
|
||||
val isSwitchable: Boolean
|
||||
get() = NativeConfig.getIsSwitchable(key)
|
||||
|
||||
var global: Boolean
|
||||
get() = NativeConfig.usingGlobal(key)
|
||||
set(value) = NativeConfig.setGlobal(key, value)
|
||||
|
||||
fun getValueAsString(needsGlobal: Boolean = false): String
|
||||
|
||||
fun reset()
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
interface AbstractShortSetting : AbstractSetting {
|
||||
val short: Short
|
||||
|
||||
fun getShort(needsGlobal: Boolean = false): Short
|
||||
fun setShort(value: Short)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
interface AbstractStringSetting : AbstractSetting {
|
||||
val string: String
|
||||
|
||||
fun getString(needsGlobal: Boolean = false): String
|
||||
fun setString(value: String)
|
||||
}
|
||||
|
@ -5,36 +5,35 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
enum class BooleanSetting(
|
||||
override val key: String,
|
||||
override val category: Settings.Category,
|
||||
override val androidDefault: Boolean? = null
|
||||
) : AbstractBooleanSetting {
|
||||
AUDIO_MUTED("audio_muted", Settings.Category.Audio),
|
||||
CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu),
|
||||
FASTMEM("cpuopt_fastmem", Settings.Category.Cpu),
|
||||
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu),
|
||||
RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core),
|
||||
USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false),
|
||||
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer),
|
||||
RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer),
|
||||
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer),
|
||||
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false),
|
||||
RENDERER_DEBUG("debug", Settings.Category.Renderer),
|
||||
PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android),
|
||||
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System);
|
||||
enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||
AUDIO_MUTED("audio_muted"),
|
||||
CPU_DEBUG_MODE("cpu_debug_mode"),
|
||||
FASTMEM("cpuopt_fastmem"),
|
||||
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
|
||||
RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
|
||||
USE_DOCKED_MODE("use_docked_mode"),
|
||||
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"),
|
||||
RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
|
||||
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
||||
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
||||
RENDERER_DEBUG("debug"),
|
||||
PICTURE_IN_PICTURE("picture_in_picture"),
|
||||
USE_CUSTOM_RTC("custom_rtc_enabled");
|
||||
|
||||
override val boolean: Boolean
|
||||
get() = NativeConfig.getBoolean(key, false)
|
||||
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
||||
NativeConfig.getBoolean(key, needsGlobal)
|
||||
|
||||
override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value)
|
||||
|
||||
override val defaultValue: Boolean by lazy {
|
||||
androidDefault ?: NativeConfig.getBoolean(key, true)
|
||||
override fun setBoolean(value: Boolean) {
|
||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
global = false
|
||||
}
|
||||
NativeConfig.setBoolean(key, value)
|
||||
}
|
||||
|
||||
override val valueAsString: String
|
||||
get() = if (boolean) "1" else "0"
|
||||
override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() }
|
||||
|
||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
||||
if (getBoolean(needsGlobal)) "1" else "0"
|
||||
|
||||
override fun reset() = NativeConfig.setBoolean(key, defaultValue)
|
||||
}
|
||||
|
@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
enum class ByteSetting(
|
||||
override val key: String,
|
||||
override val category: Settings.Category
|
||||
) : AbstractByteSetting {
|
||||
AUDIO_VOLUME("volume", Settings.Category.Audio);
|
||||
enum class ByteSetting(override val key: String) : AbstractByteSetting {
|
||||
AUDIO_VOLUME("volume");
|
||||
|
||||
override val byte: Byte
|
||||
get() = NativeConfig.getByte(key, false)
|
||||
override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
|
||||
|
||||
override fun setByte(value: Byte) = NativeConfig.setByte(key, value)
|
||||
override fun setByte(value: Byte) {
|
||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
global = false
|
||||
}
|
||||
NativeConfig.setByte(key, value)
|
||||
}
|
||||
|
||||
override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) }
|
||||
override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() }
|
||||
|
||||
override val valueAsString: String
|
||||
get() = byte.toString()
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString()
|
||||
|
||||
override fun reset() = NativeConfig.setByte(key, defaultValue)
|
||||
}
|
||||
|
@ -5,22 +5,22 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
enum class FloatSetting(
|
||||
override val key: String,
|
||||
override val category: Settings.Category
|
||||
) : AbstractFloatSetting {
|
||||
enum class FloatSetting(override val key: String) : AbstractFloatSetting {
|
||||
// No float settings currently exist
|
||||
EMPTY_SETTING("", Settings.Category.UiGeneral);
|
||||
EMPTY_SETTING("");
|
||||
|
||||
override val float: Float
|
||||
get() = NativeConfig.getFloat(key, false)
|
||||
override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false)
|
||||
|
||||
override fun setFloat(value: Float) = NativeConfig.setFloat(key, value)
|
||||
override fun setFloat(value: Float) {
|
||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
global = false
|
||||
}
|
||||
NativeConfig.setFloat(key, value)
|
||||
}
|
||||
|
||||
override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) }
|
||||
override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() }
|
||||
|
||||
override val valueAsString: String
|
||||
get() = float.toString()
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString()
|
||||
|
||||
override fun reset() = NativeConfig.setFloat(key, defaultValue)
|
||||
}
|
||||
|
@ -5,36 +5,33 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
enum class IntSetting(
|
||||
override val key: String,
|
||||
override val category: Settings.Category,
|
||||
override val androidDefault: Int? = null
|
||||
) : AbstractIntSetting {
|
||||
CPU_BACKEND("cpu_backend", Settings.Category.Cpu),
|
||||
CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
|
||||
REGION_INDEX("region_index", Settings.Category.System),
|
||||
LANGUAGE_INDEX("language_index", Settings.Category.System),
|
||||
RENDERER_BACKEND("backend", Settings.Category.Renderer),
|
||||
RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0),
|
||||
RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer),
|
||||
RENDERER_VSYNC("use_vsync", Settings.Category.Renderer),
|
||||
RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer),
|
||||
RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer),
|
||||
RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android),
|
||||
RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer),
|
||||
AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio);
|
||||
enum class IntSetting(override val key: String) : AbstractIntSetting {
|
||||
CPU_BACKEND("cpu_backend"),
|
||||
CPU_ACCURACY("cpu_accuracy"),
|
||||
REGION_INDEX("region_index"),
|
||||
LANGUAGE_INDEX("language_index"),
|
||||
RENDERER_BACKEND("backend"),
|
||||
RENDERER_ACCURACY("gpu_accuracy"),
|
||||
RENDERER_RESOLUTION("resolution_setup"),
|
||||
RENDERER_VSYNC("use_vsync"),
|
||||
RENDERER_SCALING_FILTER("scaling_filter"),
|
||||
RENDERER_ANTI_ALIASING("anti_aliasing"),
|
||||
RENDERER_SCREEN_LAYOUT("screen_layout"),
|
||||
RENDERER_ASPECT_RATIO("aspect_ratio"),
|
||||
AUDIO_OUTPUT_ENGINE("output_engine");
|
||||
|
||||
override val int: Int
|
||||
get() = NativeConfig.getInt(key, false)
|
||||
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
|
||||
|
||||
override fun setInt(value: Int) = NativeConfig.setInt(key, value)
|
||||
|
||||
override val defaultValue: Int by lazy {
|
||||
androidDefault ?: NativeConfig.getInt(key, true)
|
||||
override fun setInt(value: Int) {
|
||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
global = false
|
||||
}
|
||||
NativeConfig.setInt(key, value)
|
||||
}
|
||||
|
||||
override val valueAsString: String
|
||||
get() = int.toString()
|
||||
override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() }
|
||||
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString()
|
||||
|
||||
override fun reset() = NativeConfig.setInt(key, defaultValue)
|
||||
}
|
||||
|
@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
enum class LongSetting(
|
||||
override val key: String,
|
||||
override val category: Settings.Category
|
||||
) : AbstractLongSetting {
|
||||
CUSTOM_RTC("custom_rtc", Settings.Category.System);
|
||||
enum class LongSetting(override val key: String) : AbstractLongSetting {
|
||||
CUSTOM_RTC("custom_rtc");
|
||||
|
||||
override val long: Long
|
||||
get() = NativeConfig.getLong(key, false)
|
||||
override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal)
|
||||
|
||||
override fun setLong(value: Long) = NativeConfig.setLong(key, value)
|
||||
override fun setLong(value: Long) {
|
||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
global = false
|
||||
}
|
||||
NativeConfig.setLong(key, value)
|
||||
}
|
||||
|
||||
override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) }
|
||||
override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() }
|
||||
|
||||
override val valueAsString: String
|
||||
get() = long.toString()
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString()
|
||||
|
||||
override fun reset() = NativeConfig.setLong(key, defaultValue)
|
||||
}
|
||||
|
@ -6,62 +6,11 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
import org.yuzu.yuzu_emu.R
|
||||
|
||||
object Settings {
|
||||
enum class Category {
|
||||
Android,
|
||||
Audio,
|
||||
Core,
|
||||
Cpu,
|
||||
CpuDebug,
|
||||
CpuUnsafe,
|
||||
Renderer,
|
||||
RendererAdvanced,
|
||||
RendererDebug,
|
||||
System,
|
||||
SystemAudio,
|
||||
DataStorage,
|
||||
Debugging,
|
||||
DebuggingGraphics,
|
||||
Miscellaneous,
|
||||
Network,
|
||||
WebService,
|
||||
AddOns,
|
||||
Controls,
|
||||
Ui,
|
||||
UiGeneral,
|
||||
UiLayout,
|
||||
UiGameList,
|
||||
Screenshots,
|
||||
Shortcuts,
|
||||
Multiplayer,
|
||||
Services,
|
||||
Paths,
|
||||
MaxEnum
|
||||
}
|
||||
|
||||
val settingsList = listOf<AbstractSetting>(
|
||||
*BooleanSetting.values(),
|
||||
*ByteSetting.values(),
|
||||
*ShortSetting.values(),
|
||||
*IntSetting.values(),
|
||||
*FloatSetting.values(),
|
||||
*LongSetting.values(),
|
||||
*StringSetting.values()
|
||||
)
|
||||
|
||||
const val SECTION_GENERAL = "General"
|
||||
const val SECTION_SYSTEM = "System"
|
||||
const val SECTION_RENDERER = "Renderer"
|
||||
const val SECTION_AUDIO = "Audio"
|
||||
const val SECTION_CPU = "Cpu"
|
||||
const val SECTION_THEME = "Theme"
|
||||
const val SECTION_DEBUG = "Debug"
|
||||
|
||||
enum class MenuTag(val titleId: Int) {
|
||||
SECTION_ROOT(R.string.advanced_settings),
|
||||
SECTION_SYSTEM(R.string.preferences_system),
|
||||
SECTION_RENDERER(R.string.preferences_graphics),
|
||||
SECTION_AUDIO(R.string.preferences_audio),
|
||||
SECTION_CPU(R.string.cpu),
|
||||
SECTION_THEME(R.string.preferences_theme),
|
||||
SECTION_DEBUG(R.string.preferences_debug);
|
||||
}
|
||||
|
@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
enum class ShortSetting(
|
||||
override val key: String,
|
||||
override val category: Settings.Category
|
||||
) : AbstractShortSetting {
|
||||
RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
|
||||
enum class ShortSetting(override val key: String) : AbstractShortSetting {
|
||||
RENDERER_SPEED_LIMIT("speed_limit");
|
||||
|
||||
override val short: Short
|
||||
get() = NativeConfig.getShort(key, false)
|
||||
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
|
||||
|
||||
override fun setShort(value: Short) = NativeConfig.setShort(key, value)
|
||||
override fun setShort(value: Short) {
|
||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
global = false
|
||||
}
|
||||
NativeConfig.setShort(key, value)
|
||||
}
|
||||
|
||||
override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) }
|
||||
override val defaultValue: Short by lazy { NativeConfig.getDefaultToString(key).toShort() }
|
||||
|
||||
override val valueAsString: String
|
||||
get() = short.toString()
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
|
||||
|
||||
override fun reset() = NativeConfig.setShort(key, defaultValue)
|
||||
}
|
||||
|
@ -5,22 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
enum class StringSetting(
|
||||
override val key: String,
|
||||
override val category: Settings.Category
|
||||
) : AbstractStringSetting {
|
||||
// No string settings currently exist
|
||||
EMPTY_SETTING("", Settings.Category.UiGeneral);
|
||||
enum class StringSetting(override val key: String) : AbstractStringSetting {
|
||||
DRIVER_PATH("driver_path");
|
||||
|
||||
override val string: String
|
||||
get() = NativeConfig.getString(key, false)
|
||||
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
|
||||
|
||||
override fun setString(value: String) = NativeConfig.setString(key, value)
|
||||
override fun setString(value: String) {
|
||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
global = false
|
||||
}
|
||||
NativeConfig.setString(key, value)
|
||||
}
|
||||
|
||||
override val defaultValue: String by lazy { NativeConfig.getString(key, true) }
|
||||
override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) }
|
||||
|
||||
override val valueAsString: String
|
||||
get() = string
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal)
|
||||
|
||||
override fun reset() = NativeConfig.setString(key, defaultValue)
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ class DateTimeSetting(
|
||||
) : SettingsItem(longSetting, titleId, descriptionId) {
|
||||
override val type = TYPE_DATETIME_SETTING
|
||||
|
||||
var value: Long
|
||||
get() = longSetting.long
|
||||
set(value) = (setting as AbstractLongSetting).setLong(value)
|
||||
fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
|
||||
fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value)
|
||||
}
|
||||
|
@ -11,8 +11,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
/**
|
||||
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
|
||||
@ -31,9 +31,19 @@ abstract class SettingsItem(
|
||||
val isEditable: Boolean
|
||||
get() {
|
||||
if (!NativeLibrary.isRunning()) return true
|
||||
|
||||
// Prevent editing settings that were modified in per-game config while editing global
|
||||
// config
|
||||
if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) {
|
||||
return false
|
||||
}
|
||||
return setting.isRuntimeModifiable
|
||||
}
|
||||
|
||||
val needsRuntimeGlobal: Boolean
|
||||
get() = NativeLibrary.isRunning() && !setting.global &&
|
||||
!NativeConfig.isPerGameConfigLoaded()
|
||||
|
||||
companion object {
|
||||
const val TYPE_HEADER = 0
|
||||
const val TYPE_SWITCH = 1
|
||||
@ -48,8 +58,8 @@ abstract class SettingsItem(
|
||||
|
||||
val emptySetting = object : AbstractSetting {
|
||||
override val key: String = ""
|
||||
override val category: Settings.Category = Settings.Category.Ui
|
||||
override val defaultValue: Any = false
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = ""
|
||||
override fun reset() {}
|
||||
}
|
||||
|
||||
@ -270,9 +280,9 @@ abstract class SettingsItem(
|
||||
)
|
||||
|
||||
val fastmem = object : AbstractBooleanSetting {
|
||||
override val boolean: Boolean
|
||||
get() =
|
||||
BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
|
||||
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
||||
BooleanSetting.FASTMEM.getBoolean() &&
|
||||
BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean()
|
||||
|
||||
override fun setBoolean(value: Boolean) {
|
||||
BooleanSetting.FASTMEM.setBoolean(value)
|
||||
@ -280,9 +290,22 @@ abstract class SettingsItem(
|
||||
}
|
||||
|
||||
override val key: String = FASTMEM_COMBINED
|
||||
override val category = Settings.Category.Cpu
|
||||
override val isRuntimeModifiable: Boolean = false
|
||||
override val defaultValue: Boolean = true
|
||||
override val isSwitchable: Boolean = true
|
||||
override var global: Boolean
|
||||
get() {
|
||||
return BooleanSetting.FASTMEM.global &&
|
||||
BooleanSetting.FASTMEM_EXCLUSIVES.global
|
||||
}
|
||||
set(value) {
|
||||
BooleanSetting.FASTMEM.global = value
|
||||
BooleanSetting.FASTMEM_EXCLUSIVES.global = value
|
||||
}
|
||||
|
||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
||||
getBoolean().toString()
|
||||
|
||||
override fun reset() = setBoolean(defaultValue)
|
||||
}
|
||||
put(SwitchSetting(fastmem, R.string.fastmem, 0))
|
||||
|
@ -15,16 +15,11 @@ class SingleChoiceSetting(
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SINGLE_CHOICE
|
||||
|
||||
var selectedValue: Int
|
||||
get() {
|
||||
return when (setting) {
|
||||
is AbstractIntSetting -> setting.int
|
||||
fun getSelectedValue(needsGlobal: Boolean = false) =
|
||||
when (setting) {
|
||||
is AbstractIntSetting -> setting.getInt(needsGlobal)
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
when (setting) {
|
||||
is AbstractIntSetting -> setting.setInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value)
|
||||
}
|
||||
|
@ -20,22 +20,20 @@ class SliderSetting(
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SLIDER
|
||||
|
||||
var selectedValue: Int
|
||||
get() {
|
||||
return when (setting) {
|
||||
is AbstractByteSetting -> setting.byte.toInt()
|
||||
is AbstractShortSetting -> setting.short.toInt()
|
||||
is AbstractIntSetting -> setting.int
|
||||
is AbstractFloatSetting -> setting.float.roundToInt()
|
||||
fun getSelectedValue(needsGlobal: Boolean = false) =
|
||||
when (setting) {
|
||||
is AbstractByteSetting -> setting.getByte(needsGlobal).toInt()
|
||||
is AbstractShortSetting -> setting.getShort(needsGlobal).toInt()
|
||||
is AbstractIntSetting -> setting.getInt(needsGlobal)
|
||||
is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt()
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
|
||||
fun setSelectedValue(value: Int) =
|
||||
when (setting) {
|
||||
is AbstractByteSetting -> setting.setByte(value.toByte())
|
||||
is AbstractShortSetting -> setting.setShort(value.toShort())
|
||||
is AbstractIntSetting -> setting.setInt(value)
|
||||
is AbstractFloatSetting -> setting.setFloat(value.toFloat())
|
||||
}
|
||||
else -> (setting as AbstractIntSetting).setInt(value)
|
||||
}
|
||||
}
|
||||
|
@ -17,14 +17,13 @@ class StringSingleChoiceSetting(
|
||||
fun getValueAt(index: Int): String =
|
||||
if (index >= 0 && index < values.size) values[index] else ""
|
||||
|
||||
var selectedValue: String
|
||||
get() = stringSetting.string
|
||||
set(value) = stringSetting.setString(value)
|
||||
fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
|
||||
fun setSelectedValue(value: String) = stringSetting.setString(value)
|
||||
|
||||
val selectValueIndex: Int
|
||||
get() {
|
||||
for (i in values.indices) {
|
||||
if (values[i] == selectedValue) {
|
||||
if (values[i] == getSelectedValue()) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
@ -14,15 +14,15 @@ class SwitchSetting(
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SWITCH
|
||||
|
||||
var checked: Boolean
|
||||
get() {
|
||||
fun getIsChecked(needsGlobal: Boolean = false): Boolean {
|
||||
return when (setting) {
|
||||
is AbstractIntSetting -> setting.int == 1
|
||||
is AbstractBooleanSetting -> setting.boolean
|
||||
is AbstractIntSetting -> setting.getInt(needsGlobal) == 1
|
||||
is AbstractBooleanSetting -> setting.getBoolean(needsGlobal)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
|
||||
fun setChecked(value: Boolean) {
|
||||
when (setting) {
|
||||
is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
|
||||
is AbstractBooleanSetting -> setting.setBoolean(value)
|
||||
|
@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.navArgs
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import java.io.IOException
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
||||
@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() {
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
|
||||
SettingsFile.loadCustomConfig(args.game!!)
|
||||
}
|
||||
settingsViewModel.game = args.game
|
||||
|
||||
val navHostFragment =
|
||||
@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
// TODO: Load custom settings contextually
|
||||
if (!DirectoryInitialization.areDirectoriesReady) {
|
||||
DirectoryInitialization.start()
|
||||
}
|
||||
@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
NativeConfig.saveSettings()
|
||||
Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
||||
if (isFinishing) {
|
||||
NativeLibrary.applySettings()
|
||||
if (args.game == null) {
|
||||
NativeConfig.saveGlobalConfig()
|
||||
} else if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
NativeLibrary.logSettings()
|
||||
NativeConfig.savePerGameConfig()
|
||||
NativeConfig.unloadPerGameConfig()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
settingsViewModel.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
fun onSettingsReset() {
|
||||
// Delete settings file because the user may have changed values that do not exist in the UI
|
||||
NativeConfig.unloadConfig()
|
||||
if (args.game == null) {
|
||||
NativeConfig.unloadGlobalConfig()
|
||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
||||
if (!settingsFile.delete()) {
|
||||
throw IOException("Failed to delete $settingsFile")
|
||||
}
|
||||
NativeConfig.initializeConfig()
|
||||
NativeConfig.initializeGlobalConfig()
|
||||
} else {
|
||||
NativeConfig.unloadPerGameConfig()
|
||||
val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!)
|
||||
if (!settingsFile.delete()) {
|
||||
throw IOException("Failed to delete $settingsFile")
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
|
@ -102,8 +102,9 @@ class SettingsAdapter(
|
||||
return currentList[position].type
|
||||
}
|
||||
|
||||
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
||||
item.checked = checked
|
||||
fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) {
|
||||
item.setChecked(checked)
|
||||
notifyItemChanged(position)
|
||||
settingsViewModel.setShouldReloadSettingsList(true)
|
||||
}
|
||||
|
||||
@ -126,7 +127,7 @@ class SettingsAdapter(
|
||||
}
|
||||
|
||||
fun onDateTimeClick(item: DateTimeSetting, position: Int) {
|
||||
val storedTime = item.value * 1000
|
||||
val storedTime = item.getValue() * 1000
|
||||
|
||||
// Helper to extract hour and minute from epoch time
|
||||
val calendar: Calendar = Calendar.getInstance()
|
||||
@ -159,9 +160,9 @@ class SettingsAdapter(
|
||||
var epochTime: Long = datePicker.selection!! / 1000
|
||||
epochTime += timePicker.hour.toLong() * 60 * 60
|
||||
epochTime += timePicker.minute.toLong() * 60
|
||||
if (item.value != epochTime) {
|
||||
if (item.getValue() != epochTime) {
|
||||
notifyItemChanged(position)
|
||||
item.value = epochTime
|
||||
item.setValue(epochTime)
|
||||
}
|
||||
}
|
||||
datePicker.show(
|
||||
@ -195,6 +196,12 @@ class SettingsAdapter(
|
||||
return true
|
||||
}
|
||||
|
||||
fun onClearClick(item: SettingsItem, position: Int) {
|
||||
item.setting.global = true
|
||||
notifyItemChanged(position)
|
||||
settingsViewModel.setShouldReloadSettingsList(true)
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
|
||||
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
|
||||
return oldItem.setting.key == newItem.setting.key
|
||||
|
@ -66,7 +66,13 @@ class SettingsFragment : Fragment() {
|
||||
args.menuTag
|
||||
)
|
||||
|
||||
binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId)
|
||||
binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT &&
|
||||
args.game != null
|
||||
) {
|
||||
args.game!!.title
|
||||
} else {
|
||||
getString(args.menuTag.titleId)
|
||||
}
|
||||
binding.listSettings.apply {
|
||||
adapter = settingsAdapter
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
|
@ -7,6 +7,7 @@ import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
|
||||
@ -31,12 +32,27 @@ class SettingsFragmentPresenter(
|
||||
private val preferences: SharedPreferences
|
||||
get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
|
||||
// Extension for populating settings list based on paired settings
|
||||
// Extension for altering settings list based on each setting's properties
|
||||
fun ArrayList<SettingsItem>.add(key: String) {
|
||||
val item = SettingsItem.settingsItems[key]!!
|
||||
if (settingsViewModel.game != null && !item.setting.isSwitchable) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) {
|
||||
item.setting.global = true
|
||||
}
|
||||
|
||||
val pairedSettingKey = item.setting.pairedSettingKey
|
||||
if (pairedSettingKey.isNotEmpty()) {
|
||||
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
|
||||
val pairedSettingValue = NativeConfig.getBoolean(
|
||||
pairedSettingKey,
|
||||
if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) {
|
||||
!NativeConfig.usingGlobal(pairedSettingKey)
|
||||
} else {
|
||||
NativeConfig.usingGlobal(pairedSettingKey)
|
||||
}
|
||||
)
|
||||
if (!pairedSettingValue) return
|
||||
}
|
||||
add(item)
|
||||
@ -153,8 +169,8 @@ class SettingsFragmentPresenter(
|
||||
private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
|
||||
sl.apply {
|
||||
val theme: AbstractIntSetting = object : AbstractIntSetting {
|
||||
override val int: Int
|
||||
get() = preferences.getInt(Settings.PREF_THEME, 0)
|
||||
override fun getInt(needsGlobal: Boolean): Int =
|
||||
preferences.getInt(Settings.PREF_THEME, 0)
|
||||
|
||||
override fun setInt(value: Int) {
|
||||
preferences.edit()
|
||||
@ -164,8 +180,8 @@ class SettingsFragmentPresenter(
|
||||
}
|
||||
|
||||
override val key: String = Settings.PREF_THEME
|
||||
override val category = Settings.Category.UiGeneral
|
||||
override val isRuntimeModifiable: Boolean = false
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
|
||||
override val defaultValue: Int = 0
|
||||
override fun reset() {
|
||||
preferences.edit()
|
||||
@ -197,8 +213,8 @@ class SettingsFragmentPresenter(
|
||||
}
|
||||
|
||||
val themeMode: AbstractIntSetting = object : AbstractIntSetting {
|
||||
override val int: Int
|
||||
get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
|
||||
override fun getInt(needsGlobal: Boolean): Int =
|
||||
preferences.getInt(Settings.PREF_THEME_MODE, -1)
|
||||
|
||||
override fun setInt(value: Int) {
|
||||
preferences.edit()
|
||||
@ -208,8 +224,8 @@ class SettingsFragmentPresenter(
|
||||
}
|
||||
|
||||
override val key: String = Settings.PREF_THEME_MODE
|
||||
override val category = Settings.Category.UiGeneral
|
||||
override val isRuntimeModifiable: Boolean = false
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
|
||||
override val defaultValue: Int = -1
|
||||
override fun reset() {
|
||||
preferences.edit()
|
||||
@ -230,8 +246,8 @@ class SettingsFragmentPresenter(
|
||||
)
|
||||
|
||||
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
|
||||
override val boolean: Boolean
|
||||
get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
|
||||
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
||||
preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
|
||||
|
||||
override fun setBoolean(value: Boolean) {
|
||||
preferences.edit()
|
||||
@ -241,8 +257,10 @@ class SettingsFragmentPresenter(
|
||||
}
|
||||
|
||||
override val key: String = Settings.PREF_BLACK_BACKGROUNDS
|
||||
override val category = Settings.Category.UiGeneral
|
||||
override val isRuntimeModifiable: Boolean = false
|
||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
||||
getBoolean().toString()
|
||||
|
||||
override val defaultValue: Boolean = false
|
||||
override fun reset() {
|
||||
preferences.edit()
|
||||
|
@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||
SettingViewHolder(binding.root, adapter) {
|
||||
@ -29,12 +30,23 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
||||
}
|
||||
|
||||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
val epochTime = setting.value
|
||||
val epochTime = setting.getValue()
|
||||
val instant = Instant.ofEpochMilli(epochTime * 1000)
|
||||
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
|
||||
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
||||
binding.textSettingValue.text = dateFormatter.format(zonedTime)
|
||||
|
||||
binding.buttonClear.visibility = if (setting.setting.global ||
|
||||
!NativeConfig.isPerGameConfigLoaded()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
binding.buttonClear.setOnClickListener {
|
||||
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
binding.textSettingValue.visibility = View.GONE
|
||||
binding.buttonClear.visibility = View.GONE
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
||||
binding.textSettingName.alpha = opacity
|
||||
binding.textSettingDescription.alpha = opacity
|
||||
binding.textSettingValue.alpha = opacity
|
||||
binding.buttonClear.isEnabled = isEditable
|
||||
}
|
||||
|
||||
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
|
||||
@ -48,5 +49,6 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
||||
val opacity = if (isEditable) 1.0f else 0.5f
|
||||
binding.textSettingName.alpha = opacity
|
||||
binding.textSettingDescription.alpha = opacity
|
||||
binding.buttonClear.isEnabled = isEditable
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||
SettingViewHolder(binding.root, adapter) {
|
||||
@ -29,20 +30,31 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
|
||||
val resMgr = binding.textSettingValue.context.resources
|
||||
val values = resMgr.getIntArray(item.valuesId)
|
||||
for (i in values.indices) {
|
||||
if (values[i] == item.selectedValue) {
|
||||
if (values[i] == item.getSelectedValue()) {
|
||||
binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (item is StringSingleChoiceSetting) {
|
||||
for (i in item.values.indices) {
|
||||
if (item.values[i] == item.selectedValue) {
|
||||
if (item.values[i] == item.getSelectedValue()) {
|
||||
binding.textSettingValue.text = item.choices[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.buttonClear.visibility = if (setting.setting.global ||
|
||||
!NativeConfig.isPerGameConfigLoaded()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
binding.buttonClear.setOnClickListener {
|
||||
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||
SettingViewHolder(binding.root, adapter) {
|
||||
@ -26,10 +27,21 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
|
||||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
binding.textSettingValue.text = String.format(
|
||||
binding.textSettingValue.context.getString(R.string.value_with_units),
|
||||
setting.selectedValue,
|
||||
setting.getSelectedValue(),
|
||||
setting.units
|
||||
)
|
||||
|
||||
binding.buttonClear.visibility = if (setting.setting.global ||
|
||||
!NativeConfig.isPerGameConfigLoaded()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
binding.buttonClear.setOnClickListener {
|
||||
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
binding.textSettingValue.visibility = View.GONE
|
||||
binding.buttonClear.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
|
||||
SettingViewHolder(binding.root, adapter) {
|
||||
@ -27,9 +28,20 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
|
||||
}
|
||||
|
||||
binding.switchWidget.setOnCheckedChangeListener(null)
|
||||
binding.switchWidget.isChecked = setting.checked
|
||||
binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
|
||||
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
||||
adapter.onBooleanClick(item, binding.switchWidget.isChecked)
|
||||
adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
binding.buttonClear.visibility = if (setting.setting.global ||
|
||||
!NativeConfig.isPerGameConfigLoaded()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
binding.buttonClear.setOnClickListener {
|
||||
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
|
@ -3,15 +3,27 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.utils
|
||||
|
||||
import android.net.Uri
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import java.io.*
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
/**
|
||||
* Contains static methods for interacting with .ini files in which settings are stored.
|
||||
*/
|
||||
object SettingsFile {
|
||||
const val FILE_NAME_CONFIG = "config"
|
||||
const val FILE_NAME_CONFIG = "config.ini"
|
||||
|
||||
fun getSettingsFile(fileName: String): File =
|
||||
File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini")
|
||||
File(DirectoryInitialization.userDirectory + "/config/" + fileName)
|
||||
|
||||
fun getCustomSettingsFile(game: Game): File =
|
||||
File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini")
|
||||
|
||||
fun loadCustomConfig(game: Game) {
|
||||
val fileName = FileUtil.getFilename(Uri.parse(game.path))
|
||||
NativeConfig.initializePerGameConfig(game.programId, fileName)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@ -36,6 +37,8 @@ class DriverManagerFragment : Fragment() {
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private val driverViewModel: DriverViewModel by activityViewModels()
|
||||
|
||||
private val args by navArgs<DriverManagerFragmentArgs>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
@ -57,7 +60,9 @@ class DriverManagerFragment : Fragment() {
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
if (!driverViewModel.isInteractionAllowed) {
|
||||
driverViewModel.onOpenDriverManager(args.game)
|
||||
|
||||
if (!driverViewModel.isInteractionAllowed.value) {
|
||||
DriversLoadingDialogFragment().show(
|
||||
childFragmentManager,
|
||||
DriversLoadingDialogFragment.TAG
|
||||
@ -102,10 +107,9 @@ class DriverManagerFragment : Fragment() {
|
||||
setInsets()
|
||||
}
|
||||
|
||||
// Start installing requested driver
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
driverViewModel.onCloseDriverManager()
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
driverViewModel.onCloseDriverManager(args.game)
|
||||
}
|
||||
|
||||
private fun setInsets() =
|
||||
|
@ -47,25 +47,9 @@ class DriversLoadingDialogFragment : DialogFragment() {
|
||||
viewLifecycleOwner.lifecycleScope.apply {
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
driverViewModel.areDriversLoading.collect { checkForDismiss() }
|
||||
driverViewModel.isInteractionAllowed.collect { if (it) dismiss() }
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
driverViewModel.isDriverReady.collect { checkForDismiss() }
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForDismiss() {
|
||||
if (driverViewModel.isInteractionAllowed) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.model.EmulationViewModel
|
||||
@ -127,6 +128,16 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
return
|
||||
}
|
||||
|
||||
if (args.custom) {
|
||||
SettingsFile.loadCustomConfig(args.game!!)
|
||||
NativeConfig.unloadPerGameConfig()
|
||||
} else {
|
||||
NativeConfig.reloadGlobalConfig()
|
||||
}
|
||||
|
||||
// Install the selected driver asynchronously as the game starts
|
||||
driverViewModel.onLaunchGame()
|
||||
|
||||
// So this fragment doesn't restart on configuration changes; i.e. rotation.
|
||||
retainInstance = true
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
@ -217,6 +228,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_settings_per_game -> {
|
||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||
args.game,
|
||||
Settings.MenuTag.SECTION_ROOT
|
||||
)
|
||||
binding.root.findNavController().navigate(action)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_overlay_controls -> {
|
||||
showOverlayOptions()
|
||||
true
|
||||
@ -332,8 +352,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
driverViewModel.isDriverReady.collect {
|
||||
if (it && !emulationState.isRunning) {
|
||||
driverViewModel.isInteractionAllowed.collect {
|
||||
if (it) {
|
||||
onEmulationStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEmulationStart() {
|
||||
if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
|
||||
if (!DirectoryInitialization.areDirectoriesReady) {
|
||||
DirectoryInitialization.start()
|
||||
}
|
||||
@ -343,10 +373,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
emulationState.run(emulationActivity!!.isActivityRecreated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
@ -435,7 +461,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
private fun updateOrientation() {
|
||||
emulationActivity?.let {
|
||||
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
|
||||
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) {
|
||||
Settings.LayoutOption_MobileLandscape ->
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
Settings.LayoutOption_MobilePortrait ->
|
||||
@ -617,7 +643,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
private fun startConfiguringControls() {
|
||||
// Lock the current orientation to prevent editing inconsistencies
|
||||
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
|
||||
if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
|
||||
emulationActivity?.let {
|
||||
it.requestedOrientation =
|
||||
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
@ -635,7 +661,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
binding.doneControlConfig.visibility = View.GONE
|
||||
binding.surfaceInputOverlay.setIsInEditMode(false)
|
||||
// Unlock the orientation if it was locked for editing
|
||||
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
|
||||
if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
|
||||
emulationActivity?.let {
|
||||
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
|
||||
|
||||
class GameFolderPropertiesDialogFragment : DialogFragment() {
|
||||
@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
NativeConfig.saveGlobalConfig()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(DEEP_SCAN, deepScan)
|
||||
|
@ -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,433 @@
|
||||
// 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 (GpuDriverHelper.supportsCustomDriverLoading()) {
|
||||
add(
|
||||
SubmenuProperty(
|
||||
R.string.gpu_driver_manager,
|
||||
R.string.install_gpu_driver_description,
|
||||
R.drawable.ic_build,
|
||||
detailsFlow = driverViewModel.selectedDriverTitle
|
||||
) {
|
||||
val action = GamePropertiesFragmentDirections
|
||||
.actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
|
||||
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)
|
||||
}
|
||||
}
|
@ -68,6 +68,9 @@ class HomeSettingsFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply {
|
||||
@ -91,13 +94,14 @@ class HomeSettingsFragment : Fragment() {
|
||||
R.string.install_gpu_driver_description,
|
||||
R.drawable.ic_build,
|
||||
{
|
||||
binding.root.findNavController()
|
||||
.navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
|
||||
val action = HomeSettingsFragmentDirections
|
||||
.actionHomeSettingsFragmentToDriverManagerFragment(null)
|
||||
binding.root.findNavController().navigate(action)
|
||||
},
|
||||
{ GpuDriverHelper.supportsCustomDriverLoading() },
|
||||
R.string.custom_driver_not_supported,
|
||||
R.string.custom_driver_not_supported_description,
|
||||
driverViewModel.selectedDriverMetadata
|
||||
driverViewModel.selectedDriverTitle
|
||||
)
|
||||
)
|
||||
add(
|
||||
@ -212,8 +216,11 @@ class HomeSettingsFragment : Fragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
exitTransition = null
|
||||
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
driverViewModel.updateDriverNameForGame(null)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||
activity: FragmentActivity,
|
||||
titleId: Int,
|
||||
cancellable: Boolean = false,
|
||||
task: () -> Any
|
||||
task: suspend () -> Any
|
||||
): IndeterminateProgressDialogFragment {
|
||||
val dialog = IndeterminateProgressDialogFragment()
|
||||
val args = Bundle()
|
||||
|
@ -21,8 +21,6 @@ import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.model.Installable
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class InstallableFragment : Fragment() {
|
||||
private var _binding: FragmentInstallablesBinding? = null
|
||||
@ -75,28 +73,6 @@ class InstallableFragment : Fragment() {
|
||||
R.string.install_firmware_description,
|
||||
install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
|
||||
),
|
||||
if (mainActivity.savesFolderRoot != "") {
|
||||
Installable(
|
||||
R.string.manage_save_data,
|
||||
R.string.import_export_saves_description,
|
||||
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) },
|
||||
export = {
|
||||
mainActivity.exportSaves.launch(
|
||||
"yuzu saves - ${
|
||||
LocalDateTime.now().format(
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
||||
)
|
||||
}.zip"
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Installable(
|
||||
R.string.manage_save_data,
|
||||
R.string.import_export_saves_description,
|
||||
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }
|
||||
)
|
||||
},
|
||||
Installable(
|
||||
R.string.install_prod_keys,
|
||||
R.string.install_prod_keys_description,
|
||||
|
@ -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 helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setPositiveButton(R.string.close, null)
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
|
||||
if (titleId != 0) dialog.setTitle(titleId)
|
||||
if (titleString.isNotEmpty()) dialog.setTitle(titleString)
|
||||
if (messageDialogViewModel.positiveAction == null) {
|
||||
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) {
|
||||
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) {
|
||||
dialog.setNeutralButton(R.string.learn_more) { _, _ ->
|
||||
builder.setNeutralButton(R.string.learn_more) { _, _ ->
|
||||
openLink(getString(helpLinkId))
|
||||
}
|
||||
}
|
||||
|
||||
return dialog.show()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
messageDialogViewModel.dismissAction.invoke()
|
||||
messageDialogViewModel.clear()
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
private fun openLink(link: String) {
|
||||
@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() {
|
||||
descriptionId: Int = 0,
|
||||
descriptionString: String = "",
|
||||
helpLinkId: Int = 0,
|
||||
dismissAction: () -> Unit = {}
|
||||
positiveAction: (() -> Unit)? = null
|
||||
): MessageDialogFragment {
|
||||
val dialog = MessageDialogFragment()
|
||||
val bundle = Bundle()
|
||||
@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() {
|
||||
putString(DESCRIPTION_STRING, descriptionString)
|
||||
putInt(HELP_LINK, helpLinkId)
|
||||
}
|
||||
ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
|
||||
dismissAction
|
||||
ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
|
||||
clear()
|
||||
this.positiveAction = positiveAction
|
||||
}
|
||||
dialog.arguments = bundle
|
||||
return dialog
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.preference.PreferenceManager
|
||||
import info.debatty.java.stringsimilarity.Jaccard
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
import org.yuzu.yuzu_emu.R
|
||||
@ -60,7 +61,9 @@ class SearchFragment : Fragment() {
|
||||
// This is using the correct scope, lint is just acting up
|
||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||
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)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
@ -99,7 +102,7 @@ class SearchFragment : Fragment() {
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
gamesViewModel.games.collect { filterAndSearch() }
|
||||
gamesViewModel.games.collectLatest { filterAndSearch() }
|
||||
}
|
||||
}
|
||||
launch {
|
||||
|
@ -70,7 +70,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
sliderBinding = DialogSliderBinding.inflate(layoutInflater)
|
||||
val item = settingsViewModel.clickedItem as SliderSetting
|
||||
|
||||
settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units)
|
||||
settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
|
||||
sliderBinding.slider.apply {
|
||||
valueFrom = item.min.toFloat()
|
||||
valueTo = item.max.toFloat()
|
||||
@ -136,18 +136,18 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
is SingleChoiceSetting -> {
|
||||
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||
scSetting.selectedValue = value
|
||||
scSetting.setSelectedValue(value)
|
||||
}
|
||||
|
||||
is StringSingleChoiceSetting -> {
|
||||
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
||||
val value = scSetting.getValueAt(which)
|
||||
scSetting.selectedValue = value
|
||||
scSetting.setSelectedValue(value)
|
||||
}
|
||||
|
||||
is SliderSetting -> {
|
||||
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
||||
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
|
||||
sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
|
||||
}
|
||||
}
|
||||
closeDialog()
|
||||
@ -171,7 +171,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
}
|
||||
|
||||
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
|
||||
val value = item.selectedValue
|
||||
val value = item.getSelectedValue()
|
||||
val valuesId = item.valuesId
|
||||
if (valuesId > 0) {
|
||||
val valuesArray = requireContext().resources.getIntArray(valuesId)
|
||||
@ -211,7 +211,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
|
||||
|
||||
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
|
||||
(clickedItem as SliderSetting).selectedValue.toFloat()
|
||||
(clickedItem as SliderSetting).getSelectedValue().toFloat()
|
||||
)
|
||||
}
|
||||
settingsViewModel.clickedItem = clickedItem
|
||||
|
@ -304,6 +304,11 @@ class SetupFragment : Fragment() {
|
||||
setInsets()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
NativeConfig.saveGlobalConfig()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
if (_binding != 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
|
||||
}
|
||||
}
|
@ -7,60 +7,55 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
|
||||
class DriverViewModel : ViewModel() {
|
||||
private val _areDriversLoading = MutableStateFlow(false)
|
||||
val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
|
||||
|
||||
private val _isDriverReady = MutableStateFlow(true)
|
||||
val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
|
||||
|
||||
private val _isDeletingDrivers = MutableStateFlow(false)
|
||||
val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers
|
||||
|
||||
private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>())
|
||||
val isInteractionAllowed: StateFlow<Boolean> =
|
||||
combine(
|
||||
_areDriversLoading,
|
||||
_isDriverReady,
|
||||
_isDeletingDrivers
|
||||
) { loading, ready, deleting ->
|
||||
!loading && ready && !deleting
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false)
|
||||
|
||||
private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers())
|
||||
val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
|
||||
|
||||
var previouslySelectedDriver = 0
|
||||
var selectedDriver = -1
|
||||
|
||||
private val _selectedDriverMetadata =
|
||||
MutableStateFlow(
|
||||
GpuDriverHelper.customDriverData.name
|
||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||
)
|
||||
val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
|
||||
// Used for showing which driver is currently installed within the driver manager card
|
||||
private val _selectedDriverTitle = MutableStateFlow("")
|
||||
val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
|
||||
|
||||
private val _newDriverInstalled = MutableStateFlow(false)
|
||||
val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
|
||||
|
||||
val driversToDelete = mutableListOf<String>()
|
||||
|
||||
val isInteractionAllowed
|
||||
get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
|
||||
|
||||
init {
|
||||
_areDriversLoading.value = true
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val drivers = GpuDriverHelper.getDrivers()
|
||||
val currentDriverMetadata = GpuDriverHelper.customDriverData
|
||||
for (i in drivers.indices) {
|
||||
if (drivers[i].second == currentDriverMetadata) {
|
||||
setSelectedDriverIndex(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData
|
||||
findSelectedDriver(currentDriverMetadata)
|
||||
|
||||
// If a user had installed a driver before the manager was implemented, this zips
|
||||
// the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
|
||||
@ -74,14 +69,21 @@ class DriverViewModel : ViewModel() {
|
||||
GpuDriverHelper.driverInstallationPath!!,
|
||||
BufferedOutputStream(driverToSave.outputStream())
|
||||
)
|
||||
drivers.add(Pair(driverToSave.path, currentDriverMetadata))
|
||||
setSelectedDriverIndex(drivers.size - 1)
|
||||
_driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
|
||||
setSelectedDriverIndex(_driverList.value.size - 1)
|
||||
}
|
||||
|
||||
_driverList.value = drivers
|
||||
_areDriversLoading.value = false
|
||||
}
|
||||
// If a user had installed a driver before the config was reworked to be multiplatform,
|
||||
// we have save the path of the previously selected driver to the new setting.
|
||||
if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 &&
|
||||
StringSetting.DRIVER_PATH.global
|
||||
) {
|
||||
StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first)
|
||||
NativeConfig.saveGlobalConfig()
|
||||
} else {
|
||||
findSelectedDriver(GpuDriverHelper.customDriverSettingData)
|
||||
}
|
||||
updateDriverNameForGame(null)
|
||||
}
|
||||
|
||||
fun setSelectedDriverIndex(value: Int) {
|
||||
@ -98,9 +100,9 @@ class DriverViewModel : ViewModel() {
|
||||
fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
|
||||
val driverIndex = _driverList.value.indexOfFirst { it == driverData }
|
||||
if (driverIndex == -1) {
|
||||
setSelectedDriverIndex(_driverList.value.size)
|
||||
_driverList.value.add(driverData)
|
||||
_selectedDriverMetadata.value = driverData.second.name
|
||||
setSelectedDriverIndex(_driverList.value.size - 1)
|
||||
_selectedDriverTitle.value = driverData.second.name
|
||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||
} else {
|
||||
setSelectedDriverIndex(driverIndex)
|
||||
@ -111,8 +113,31 @@ class DriverViewModel : ViewModel() {
|
||||
_driverList.value.remove(driverData)
|
||||
}
|
||||
|
||||
fun onCloseDriverManager() {
|
||||
fun onOpenDriverManager(game: Game?) {
|
||||
if (game != null) {
|
||||
SettingsFile.loadCustomConfig(game)
|
||||
}
|
||||
|
||||
val driverPath = StringSetting.DRIVER_PATH.getString()
|
||||
if (driverPath.isEmpty()) {
|
||||
setSelectedDriverIndex(0)
|
||||
} else {
|
||||
findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath)))
|
||||
}
|
||||
}
|
||||
|
||||
fun onCloseDriverManager(game: Game?) {
|
||||
_isDeletingDrivers.value = true
|
||||
StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)
|
||||
updateDriverNameForGame(game)
|
||||
if (game == null) {
|
||||
NativeConfig.saveGlobalConfig()
|
||||
} else {
|
||||
NativeConfig.savePerGameConfig()
|
||||
NativeConfig.unloadPerGameConfig()
|
||||
NativeConfig.reloadGlobalConfig()
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
driversToDelete.forEach {
|
||||
@ -125,23 +150,29 @@ class DriverViewModel : ViewModel() {
|
||||
_isDeletingDrivers.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) {
|
||||
// It is the Emulation Fragment's responsibility to load per-game settings so that this function
|
||||
// knows what driver to load.
|
||||
fun onLaunchGame() {
|
||||
_isDriverReady.value = false
|
||||
|
||||
val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString())
|
||||
val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData
|
||||
if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) {
|
||||
return
|
||||
}
|
||||
|
||||
_isDriverReady.value = false
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (selectedDriver == 0) {
|
||||
if (selectedDriverMetadata.name == null) {
|
||||
GpuDriverHelper.installDefaultDriver()
|
||||
setDriverReady()
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val driverToInstall = File(driverList.value[selectedDriver].first)
|
||||
if (driverToInstall.exists()) {
|
||||
GpuDriverHelper.installCustomDriver(driverToInstall)
|
||||
if (selectedDriverFile.exists()) {
|
||||
GpuDriverHelper.installCustomDriver(selectedDriverFile)
|
||||
} else {
|
||||
GpuDriverHelper.installDefaultDriver()
|
||||
}
|
||||
@ -150,9 +181,43 @@ class DriverViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) {
|
||||
if (driverList.value.size == 1) {
|
||||
setSelectedDriverIndex(0)
|
||||
return
|
||||
}
|
||||
|
||||
driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> ->
|
||||
if (driver.second == currentDriverMetadata) {
|
||||
setSelectedDriverIndex(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDriverNameForGame(game: Game?) {
|
||||
if (!GpuDriverHelper.supportsCustomDriverLoading()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (game == null || NativeConfig.isPerGameConfigLoaded()) {
|
||||
updateName()
|
||||
} else {
|
||||
SettingsFile.loadCustomConfig(game)
|
||||
updateName()
|
||||
NativeConfig.unloadPerGameConfig()
|
||||
NativeConfig.reloadGlobalConfig()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateName() {
|
||||
_selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
|
||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||
}
|
||||
|
||||
private fun setDriverReady() {
|
||||
_isDriverReady.value = true
|
||||
_selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name
|
||||
_selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
|
||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,18 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import java.util.HashSet
|
||||
import kotlinx.parcelize.Parcelize
|
||||
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
|
||||
@Serializable
|
||||
@ -15,12 +23,44 @@ class Game(
|
||||
val path: String,
|
||||
val programId: String = "",
|
||||
val developer: String = "",
|
||||
val version: String = "",
|
||||
var version: String = "",
|
||||
val isHomebrew: Boolean = false
|
||||
) : Parcelable {
|
||||
val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
|
||||
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 {
|
||||
if (other !is Game) {
|
||||
return false
|
||||
@ -34,6 +74,7 @@ class Game(
|
||||
result = 31 * result + path.hashCode()
|
||||
result = 31 * result + programId.hashCode()
|
||||
result = 31 * result + developer.hashCode()
|
||||
result = 31 * result + version.hashCode()
|
||||
result = 31 * result + isHomebrew.hashCode()
|
||||
return result
|
||||
}
|
||||
|
@ -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
|
@ -20,8 +20,8 @@ import kotlinx.serialization.json.Json
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class GamesViewModel : ViewModel() {
|
||||
val games: StateFlow<List<Game>> get() = _games
|
||||
@ -33,6 +33,8 @@ class GamesViewModel : ViewModel() {
|
||||
val isReloading: StateFlow<Boolean> get() = _isReloading
|
||||
private val _isReloading = MutableStateFlow(false)
|
||||
|
||||
private val reloading = AtomicBoolean(false)
|
||||
|
||||
val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData
|
||||
private val _shouldSwapData = MutableStateFlow(false)
|
||||
|
||||
@ -49,38 +51,8 @@ class GamesViewModel : ViewModel() {
|
||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||
NativeLibrary.reloadKeys()
|
||||
|
||||
// Retrieve list of cached games
|
||||
val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
.getStringSet(GameHelper.KEY_GAMES, emptySet())
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
getGameDirs()
|
||||
if (storedGames!!.isNotEmpty()) {
|
||||
val deserializedGames = mutableSetOf<Game>()
|
||||
storedGames.forEach {
|
||||
val game: Game
|
||||
try {
|
||||
game = Json.decodeFromString(it)
|
||||
} catch (e: Exception) {
|
||||
// We don't care about any errors related to parsing the game cache
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val gameExists =
|
||||
DocumentFile.fromSingleUri(
|
||||
YuzuApplication.appContext,
|
||||
Uri.parse(game.path)
|
||||
)?.exists()
|
||||
if (gameExists == true) {
|
||||
deserializedGames.add(game)
|
||||
}
|
||||
}
|
||||
setGames(deserializedGames.toList())
|
||||
}
|
||||
reloadGames(false)
|
||||
}
|
||||
}
|
||||
reloadGames(directoriesChanged = false, firstStartup = true)
|
||||
}
|
||||
|
||||
fun setGames(games: List<Game>) {
|
||||
@ -110,16 +82,46 @@ class GamesViewModel : ViewModel() {
|
||||
_searchFocused.value = searchFocused
|
||||
}
|
||||
|
||||
fun reloadGames(directoriesChanged: Boolean) {
|
||||
if (isReloading.value) {
|
||||
fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
|
||||
if (reloading.get()) {
|
||||
return
|
||||
}
|
||||
reloading.set(true)
|
||||
_isReloading.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
GameMetadata.resetMetadata()
|
||||
if (firstStartup) {
|
||||
// Retrieve list of cached games
|
||||
val storedGames =
|
||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
.getStringSet(GameHelper.KEY_GAMES, emptySet())
|
||||
if (storedGames!!.isNotEmpty()) {
|
||||
val deserializedGames = mutableSetOf<Game>()
|
||||
storedGames.forEach {
|
||||
val game: Game
|
||||
try {
|
||||
game = Json.decodeFromString(it)
|
||||
} catch (e: Exception) {
|
||||
// We don't care about any errors related to parsing the game cache
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val gameExists =
|
||||
DocumentFile.fromSingleUri(
|
||||
YuzuApplication.appContext,
|
||||
Uri.parse(game.path)
|
||||
)?.exists()
|
||||
if (gameExists == true) {
|
||||
deserializedGames.add(game)
|
||||
}
|
||||
}
|
||||
setGames(deserializedGames.toList())
|
||||
}
|
||||
}
|
||||
|
||||
setGames(GameHelper.getGames())
|
||||
reloading.set(false)
|
||||
_isReloading.value = false
|
||||
|
||||
if (directoriesChanged) {
|
||||
@ -168,6 +170,7 @@ class GamesViewModel : ViewModel() {
|
||||
fun onCloseGameFoldersFragment() =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
NativeConfig.saveGlobalConfig()
|
||||
getGameDirs(true)
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@ -21,6 +22,12 @@ class HomeViewModel : ViewModel() {
|
||||
private val _gamesDirSelected = MutableStateFlow(false)
|
||||
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
|
||||
|
||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||
@ -44,4 +51,12 @@ class HomeViewModel : ViewModel() {
|
||||
fun setGamesDirSelected(selected: Boolean) {
|
||||
_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
|
||||
|
||||
class MessageDialogViewModel : ViewModel() {
|
||||
var dismissAction: () -> Unit = {}
|
||||
var positiveAction: (() -> Unit)? = null
|
||||
|
||||
fun clear() {
|
||||
dismissAction = {}
|
||||
positiveAction = null
|
||||
}
|
||||
}
|
||||
|
@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() {
|
||||
fun setAdapterItemChanged(value: Int) {
|
||||
_adapterItemChanged.value = value
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
game = null
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() {
|
||||
val cancelled: StateFlow<Boolean> get() = _cancelled
|
||||
private val _cancelled = MutableStateFlow(false)
|
||||
|
||||
lateinit var task: () -> Any
|
||||
lateinit var task: suspend () -> Any
|
||||
|
||||
fun clear() {
|
||||
_result.value = Any()
|
||||
|
@ -19,7 +19,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||
@ -35,11 +35,6 @@ class GamesFragment : Fragment() {
|
||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialFadeThrough()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@ -52,7 +47,9 @@ class GamesFragment : Fragment() {
|
||||
// This is using the correct scope, lint is just acting up
|
||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||
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 {
|
||||
layoutManager = AutofitGridLayoutManager(
|
||||
@ -99,7 +96,7 @@ class GamesFragment : Fragment() {
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
gamesViewModel.games.collect {
|
||||
gamesViewModel.games.collectLatest {
|
||||
(binding.gridGames.adapter as GameAdapter).submitList(it)
|
||||
if (it.isEmpty()) {
|
||||
binding.noticeText.visibility = View.VISIBLE
|
||||
|
@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.io.File
|
||||
import java.io.FilenameFilter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
@ -43,7 +40,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
||||
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
||||
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.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.model.TaskState
|
||||
@ -60,15 +57,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
private val homeViewModel: HomeViewModel by viewModels()
|
||||
private val gamesViewModel: GamesViewModel by viewModels()
|
||||
private val taskViewModel: TaskViewModel by viewModels()
|
||||
private val addonViewModel: AddonViewModel by viewModels()
|
||||
|
||||
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?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
||||
@ -145,6 +137,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
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)
|
||||
@ -253,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
NativeConfig.saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
EmulationActivity.stopForegroundService(this)
|
||||
super.onDestroy()
|
||||
@ -468,7 +463,46 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
val installGameUpdate = registerForActivityResult(
|
||||
ActivityResultContracts.OpenMultipleDocuments()
|
||||
) { documents: List<Uri> ->
|
||||
if (documents.isNotEmpty()) {
|
||||
if (documents.isEmpty()) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
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
|
||||
@ -507,6 +541,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
}
|
||||
}
|
||||
|
||||
addonViewModel.refreshAddons()
|
||||
|
||||
val separator = System.getProperty("line.separator") ?: "\n"
|
||||
val installResult = StringBuilder()
|
||||
if (installSuccess > 0) {
|
||||
@ -572,7 +608,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
}
|
||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
val exportUserData = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/zip")
|
||||
@ -632,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
}
|
||||
|
||||
// Clear existing user data
|
||||
NativeConfig.unloadConfig()
|
||||
NativeConfig.unloadGlobalConfig()
|
||||
File(DirectoryInitialization.userDirectory!!).deleteRecursively()
|
||||
|
||||
// Copy archive to internal storage
|
||||
@ -651,108 +686,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
|
||||
// Reinitialize relevant data
|
||||
NativeLibrary.initializeSystem(true)
|
||||
NativeConfig.initializeConfig()
|
||||
NativeConfig.initializeGlobalConfig()
|
||||
gamesViewModel.reloadGames(false)
|
||||
|
||||
return@newInstance getString(R.string.user_data_import_success)
|
||||
}.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")
|
||||
}
|
@ -16,7 +16,7 @@ object DirectoryInitialization {
|
||||
if (!areDirectoriesReady) {
|
||||
initializeInternalStorage()
|
||||
NativeLibrary.initializeSystem(false)
|
||||
NativeConfig.initializeConfig()
|
||||
NativeConfig.initializeGlobalConfig()
|
||||
areDirectoriesReady = true
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import java.io.BufferedOutputStream
|
||||
import java.lang.NullPointerException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.IllegalStateException
|
||||
|
||||
object FileUtil {
|
||||
const val PATH_TREE = "tree"
|
||||
@ -342,6 +343,37 @@ object FileUtil {
|
||||
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 {
|
||||
val paths = uri.pathSegments
|
||||
return paths.size == 2 && PATH_TREE == paths[0]
|
||||
|
@ -36,6 +36,12 @@ object GameHelper {
|
||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||
NativeLibrary.reloadKeys()
|
||||
|
||||
// Reset metadata so we don't use stale information
|
||||
GameMetadata.resetMetadata()
|
||||
|
||||
// Remove previous filesystem provider information so we can get up to date version info
|
||||
NativeLibrary.clearFilesystemProvider()
|
||||
|
||||
val badDirs = mutableListOf<Int>()
|
||||
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||
val gameDirUri = Uri.parse(gameDir.uriString)
|
||||
@ -92,14 +98,24 @@ object GameHelper {
|
||||
)
|
||||
} else {
|
||||
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
|
||||
games.add(getGame(it.uri, true))
|
||||
val game = getGame(it.uri, true)
|
||||
if (game != null) {
|
||||
games.add(game)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
|
||||
fun getGame(uri: Uri, addedToLibrary: Boolean): Game? {
|
||||
val filePath = uri.toString()
|
||||
if (!GameMetadata.getIsValid(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Needed to update installed content information
|
||||
NativeLibrary.addFileToFilesystemProvider(filePath)
|
||||
|
||||
var name = GameMetadata.getTitle(filePath)
|
||||
|
||||
// If the game's title field is empty, use the filename.
|
||||
@ -118,7 +134,7 @@ object GameHelper {
|
||||
filePath,
|
||||
programId,
|
||||
GameMetadata.getDeveloper(filePath),
|
||||
GameMetadata.getVersion(filePath),
|
||||
GameMetadata.getVersion(filePath, false),
|
||||
GameMetadata.getIsHomebrew(filePath)
|
||||
)
|
||||
|
||||
|
@ -4,13 +4,15 @@
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
object GameMetadata {
|
||||
external fun getIsValid(path: String): Boolean
|
||||
|
||||
external fun getTitle(path: String): String
|
||||
|
||||
external fun getProgramId(path: String): String
|
||||
|
||||
external fun getDeveloper(path: String): String
|
||||
|
||||
external fun getVersion(path: String): String
|
||||
external fun getVersion(path: String, reload: Boolean): String
|
||||
|
||||
external fun getIcon(path: String): ByteArray
|
||||
|
||||
|
@ -10,6 +10,8 @@ import java.io.File
|
||||
import java.io.IOException
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.zip.ZipException
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
@ -44,7 +46,7 @@ object GpuDriverHelper {
|
||||
NativeLibrary.initializeGpuDriver(
|
||||
hookLibPath,
|
||||
driverInstallationPath,
|
||||
customDriverData.libraryName,
|
||||
installedCustomDriverData.libraryName,
|
||||
fileRedirectionPath
|
||||
)
|
||||
}
|
||||
@ -190,6 +192,7 @@ object GpuDriverHelper {
|
||||
}
|
||||
}
|
||||
} catch (_: ZipException) {
|
||||
} catch (_: FileNotFoundException) {
|
||||
}
|
||||
return GpuDriverMetadata()
|
||||
}
|
||||
@ -197,9 +200,12 @@ object GpuDriverHelper {
|
||||
external fun supportsCustomDriverLoading(): Boolean
|
||||
|
||||
// Parse the custom driver metadata to retrieve the name.
|
||||
val customDriverData: GpuDriverMetadata
|
||||
val installedCustomDriverData: GpuDriverMetadata
|
||||
get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
|
||||
|
||||
val customDriverSettingData: GpuDriverMetadata
|
||||
get() = getMetadataFromZip(File(StringSetting.DRIVER_PATH.getString()))
|
||||
|
||||
fun initializeDirectories() {
|
||||
// Ensure the file redirection directory exists.
|
||||
val fileRedirectionDir = File(fileRedirectionPath!!)
|
||||
|
@ -27,13 +27,13 @@ object MemoryUtil {
|
||||
const val Pb = Tb * 1024
|
||||
const val Eb = Pb * 1024
|
||||
|
||||
private fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
|
||||
fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
|
||||
when {
|
||||
size < Kb -> {
|
||||
context.getString(
|
||||
R.string.memory_formatted,
|
||||
size.hundredths,
|
||||
context.getString(R.string.memory_byte)
|
||||
context.getString(R.string.memory_byte_shorthand)
|
||||
)
|
||||
}
|
||||
size < Mb -> {
|
||||
|
@ -7,56 +7,111 @@ import org.yuzu.yuzu_emu.model.GameDir
|
||||
|
||||
object NativeConfig {
|
||||
/**
|
||||
* Creates a Config object and opens the emulation config.
|
||||
* Loads global config.
|
||||
*/
|
||||
@Synchronized
|
||||
external fun initializeConfig()
|
||||
external fun initializeGlobalConfig()
|
||||
|
||||
/**
|
||||
* Destroys the stored config object. This automatically saves the existing config.
|
||||
* Destroys the stored global config object. This does not save the existing config.
|
||||
*/
|
||||
@Synchronized
|
||||
external fun unloadConfig()
|
||||
external fun unloadGlobalConfig()
|
||||
|
||||
/**
|
||||
* Reads values saved to the config file and saves them.
|
||||
* Reads values in the global config file and saves them.
|
||||
*/
|
||||
@Synchronized
|
||||
external fun reloadSettings()
|
||||
external fun reloadGlobalConfig()
|
||||
|
||||
/**
|
||||
* Saves settings values in memory to disk.
|
||||
* Saves global settings values in memory to disk.
|
||||
*/
|
||||
@Synchronized
|
||||
external fun saveSettings()
|
||||
external fun saveGlobalConfig()
|
||||
|
||||
external fun getBoolean(key: String, getDefault: Boolean): Boolean
|
||||
/**
|
||||
* Creates per-game config for the specified parameters. Must be unloaded once per-game config
|
||||
* is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets
|
||||
* will follow the per-game config until the global config is reloaded.
|
||||
*
|
||||
* @param programId String representation of the u64 programId
|
||||
* @param fileName Filename of the game, including its extension
|
||||
*/
|
||||
@Synchronized
|
||||
external fun initializePerGameConfig(programId: String, fileName: String)
|
||||
|
||||
@Synchronized
|
||||
external fun isPerGameConfigLoaded(): Boolean
|
||||
|
||||
/**
|
||||
* Saves per-game settings values in memory to disk.
|
||||
*/
|
||||
@Synchronized
|
||||
external fun savePerGameConfig()
|
||||
|
||||
/**
|
||||
* Destroys the stored per-game config object. This does not save the config.
|
||||
*/
|
||||
@Synchronized
|
||||
external fun unloadPerGameConfig()
|
||||
|
||||
@Synchronized
|
||||
external fun getBoolean(key: String, needsGlobal: Boolean): Boolean
|
||||
|
||||
@Synchronized
|
||||
external fun setBoolean(key: String, value: Boolean)
|
||||
|
||||
external fun getByte(key: String, getDefault: Boolean): Byte
|
||||
@Synchronized
|
||||
external fun getByte(key: String, needsGlobal: Boolean): Byte
|
||||
|
||||
@Synchronized
|
||||
external fun setByte(key: String, value: Byte)
|
||||
|
||||
external fun getShort(key: String, getDefault: Boolean): Short
|
||||
@Synchronized
|
||||
external fun getShort(key: String, needsGlobal: Boolean): Short
|
||||
|
||||
@Synchronized
|
||||
external fun setShort(key: String, value: Short)
|
||||
|
||||
external fun getInt(key: String, getDefault: Boolean): Int
|
||||
@Synchronized
|
||||
external fun getInt(key: String, needsGlobal: Boolean): Int
|
||||
|
||||
@Synchronized
|
||||
external fun setInt(key: String, value: Int)
|
||||
|
||||
external fun getFloat(key: String, getDefault: Boolean): Float
|
||||
@Synchronized
|
||||
external fun getFloat(key: String, needsGlobal: Boolean): Float
|
||||
|
||||
@Synchronized
|
||||
external fun setFloat(key: String, value: Float)
|
||||
|
||||
external fun getLong(key: String, getDefault: Boolean): Long
|
||||
@Synchronized
|
||||
external fun getLong(key: String, needsGlobal: Boolean): Long
|
||||
|
||||
@Synchronized
|
||||
external fun setLong(key: String, value: Long)
|
||||
|
||||
external fun getString(key: String, getDefault: Boolean): String
|
||||
@Synchronized
|
||||
external fun getString(key: String, needsGlobal: Boolean): String
|
||||
|
||||
@Synchronized
|
||||
external fun setString(key: String, value: String)
|
||||
|
||||
external fun getIsRuntimeModifiable(key: String): Boolean
|
||||
|
||||
external fun getConfigHeader(category: Int): String
|
||||
|
||||
external fun getPairedSettingKey(key: String): String
|
||||
|
||||
external fun getIsSwitchable(key: String): Boolean
|
||||
|
||||
@Synchronized
|
||||
external fun usingGlobal(key: String): Boolean
|
||||
|
||||
@Synchronized
|
||||
external fun setGlobal(key: String, global: Boolean)
|
||||
|
||||
external fun getDefaultToString(key: String): String
|
||||
|
||||
/**
|
||||
* Gets every [GameDir] in AndroidSettings::values.game_dirs
|
||||
*/
|
||||
@ -74,4 +129,23 @@ object NativeConfig {
|
||||
*/
|
||||
@Synchronized
|
||||
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>)
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ void AndroidConfig::ReadAndroidValues() {
|
||||
ReadAndroidUIValues();
|
||||
ReadUIValues();
|
||||
}
|
||||
ReadDriverValues();
|
||||
}
|
||||
|
||||
void AndroidConfig::ReadAndroidUIValues() {
|
||||
@ -57,6 +58,7 @@ void AndroidConfig::ReadUIValues() {
|
||||
void AndroidConfig::ReadPathValues() {
|
||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
||||
|
||||
AndroidSettings::values.game_dirs.clear();
|
||||
const int gamedirs_size = BeginArray(std::string("gamedirs"));
|
||||
for (int i = 0; i < gamedirs_size; ++i) {
|
||||
SetArrayIndex(i);
|
||||
@ -71,11 +73,20 @@ void AndroidConfig::ReadPathValues() {
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
void AndroidConfig::ReadDriverValues() {
|
||||
BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
|
||||
|
||||
ReadCategory(Settings::Category::GpuDriver);
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
void AndroidConfig::SaveAndroidValues() {
|
||||
if (global) {
|
||||
SaveAndroidUIValues();
|
||||
SaveUIValues();
|
||||
}
|
||||
SaveDriverValues();
|
||||
|
||||
WriteToIni();
|
||||
}
|
||||
@ -111,6 +122,14 @@ void AndroidConfig::SavePathValues() {
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
void AndroidConfig::SaveDriverValues() {
|
||||
BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
|
||||
|
||||
WriteCategory(Settings::Category::GpuDriver);
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
|
||||
auto& map = Settings::values.linkage.by_category;
|
||||
if (map.contains(category)) {
|
||||
|
@ -17,6 +17,7 @@ public:
|
||||
protected:
|
||||
void ReadAndroidValues();
|
||||
void ReadAndroidUIValues();
|
||||
void ReadDriverValues();
|
||||
void ReadHidbusValues() override {}
|
||||
void ReadDebugControlValues() override {}
|
||||
void ReadPathValues() override;
|
||||
@ -28,6 +29,7 @@ protected:
|
||||
|
||||
void SaveAndroidValues();
|
||||
void SaveAndroidUIValues();
|
||||
void SaveDriverValues();
|
||||
void SaveHidbusValues() override {}
|
||||
void SaveDebugControlValues() override {}
|
||||
void SavePathValues() override;
|
||||
|
@ -30,6 +30,9 @@ struct Values {
|
||||
Settings::Specialization::Default,
|
||||
true,
|
||||
true};
|
||||
|
||||
Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path",
|
||||
Settings::Category::GpuDriver};
|
||||
};
|
||||
|
||||
extern Values values;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <core/core.h>
|
||||
#include <core/file_sys/mode.h>
|
||||
#include <core/file_sys/patch_manager.h>
|
||||
#include <core/loader/nro.h>
|
||||
#include <jni.h>
|
||||
@ -61,7 +62,11 @@ RomMetadata CacheRomMetadata(const std::string& path) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
RomMetadata GetRomMetadata(const std::string& path) {
|
||||
RomMetadata GetRomMetadata(const std::string& path, bool reload = false) {
|
||||
if (reload) {
|
||||
return CacheRomMetadata(path);
|
||||
}
|
||||
|
||||
if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
|
||||
return search->second;
|
||||
}
|
||||
@ -71,6 +76,32 @@ RomMetadata GetRomMetadata(const std::string& path) {
|
||||
|
||||
extern "C" {
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobject obj,
|
||||
jstring jpath) {
|
||||
const auto file = EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(
|
||||
GetJString(env, jpath), FileSys::Mode::Read);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file);
|
||||
if (!loader) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto fileType = loader->GetFileType();
|
||||
if (fileType == Loader::FileType::Unknown || fileType == Loader::FileType::Error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
u64 programId = 0;
|
||||
Loader::ResultStatus res = loader->ReadProgramId(programId);
|
||||
if (res != Loader::ResultStatus::Success) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj,
|
||||
jstring jpath) {
|
||||
return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title);
|
||||
@ -87,8 +118,8 @@ jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getDeveloper(JNIEnv* env, job
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj,
|
||||
jstring jpath) {
|
||||
return ToJString(env, GetRomMetadata(GetJString(env, jpath)).version);
|
||||
jstring jpath, jboolean jreload) {
|
||||
return ToJString(env, GetRomMetadata(GetJString(env, jpath), jreload).version);
|
||||
}
|
||||
|
||||
jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj,
|
||||
@ -106,7 +137,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsHomebrew(JNIEnv* env, j
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) {
|
||||
return m_rom_metadata_cache.clear();
|
||||
m_rom_metadata_cache.clear();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
@ -20,6 +20,21 @@ static jmethodID s_disk_cache_load_progress;
|
||||
static jmethodID s_on_emulation_started;
|
||||
static jmethodID s_on_emulation_stopped;
|
||||
|
||||
static jclass s_game_class;
|
||||
static jmethodID s_game_constructor;
|
||||
static jfieldID s_game_title_field;
|
||||
static jfieldID s_game_path_field;
|
||||
static jfieldID s_game_program_id_field;
|
||||
static jfieldID s_game_developer_field;
|
||||
static jfieldID s_game_version_field;
|
||||
static jfieldID s_game_is_homebrew_field;
|
||||
|
||||
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;
|
||||
|
||||
namespace IDCache {
|
||||
@ -79,6 +94,58 @@ jmethodID GetOnEmulationStopped() {
|
||||
return s_on_emulation_stopped;
|
||||
}
|
||||
|
||||
jclass GetGameClass() {
|
||||
return s_game_class;
|
||||
}
|
||||
|
||||
jmethodID GetGameConstructor() {
|
||||
return s_game_constructor;
|
||||
}
|
||||
|
||||
jfieldID GetGameTitleField() {
|
||||
return s_game_title_field;
|
||||
}
|
||||
|
||||
jfieldID GetGamePathField() {
|
||||
return s_game_path_field;
|
||||
}
|
||||
|
||||
jfieldID GetGameProgramIdField() {
|
||||
return s_game_program_id_field;
|
||||
}
|
||||
|
||||
jfieldID GetGameDeveloperField() {
|
||||
return s_game_developer_field;
|
||||
}
|
||||
|
||||
jfieldID GetGameVersionField() {
|
||||
return s_game_version_field;
|
||||
}
|
||||
|
||||
jfieldID GetGameIsHomebrewField() {
|
||||
return s_game_is_homebrew_field;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
#ifdef __cplusplus
|
||||
@ -115,6 +182,31 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
s_on_emulation_stopped =
|
||||
env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
|
||||
|
||||
const jclass game_class = env->FindClass("org/yuzu/yuzu_emu/model/Game");
|
||||
s_game_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_class));
|
||||
s_game_constructor = env->GetMethodID(game_class, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/"
|
||||
"String;Ljava/lang/String;Ljava/lang/String;Z)V");
|
||||
s_game_title_field = env->GetFieldID(game_class, "title", "Ljava/lang/String;");
|
||||
s_game_path_field = env->GetFieldID(game_class, "path", "Ljava/lang/String;");
|
||||
s_game_program_id_field = env->GetFieldID(game_class, "programId", "Ljava/lang/String;");
|
||||
s_game_developer_field = env->GetFieldID(game_class, "developer", "Ljava/lang/String;");
|
||||
s_game_version_field = env->GetFieldID(game_class, "version", "Ljava/lang/String;");
|
||||
s_game_is_homebrew_field = env->GetFieldID(game_class, "isHomebrew", "Z");
|
||||
env->DeleteLocalRef(game_class);
|
||||
|
||||
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
|
||||
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
||||
|
||||
@ -136,6 +228,9 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
||||
env->DeleteGlobalRef(s_load_callback_stage_class);
|
||||
env->DeleteGlobalRef(s_game_dir_class);
|
||||
env->DeleteGlobalRef(s_game_class);
|
||||
env->DeleteGlobalRef(s_string_class);
|
||||
env->DeleteGlobalRef(s_pair_class);
|
||||
|
||||
// UnInitialize applets
|
||||
SoftwareKeyboard::CleanupJNI(env);
|
||||
|
@ -20,4 +20,19 @@ jmethodID GetDiskCacheLoadProgress();
|
||||
jmethodID GetOnEmulationStarted();
|
||||
jmethodID GetOnEmulationStopped();
|
||||
|
||||
jclass GetGameClass();
|
||||
jmethodID GetGameConstructor();
|
||||
jfieldID GetGameTitleField();
|
||||
jfieldID GetGamePathField();
|
||||
jfieldID GetGameProgramIdField();
|
||||
jfieldID GetGameDeveloperField();
|
||||
jfieldID GetGameVersionField();
|
||||
jfieldID GetGameIsHomebrewField();
|
||||
|
||||
jclass GetStringClass();
|
||||
jclass GetPairClass();
|
||||
jmethodID GetPairConstructor();
|
||||
jfieldID GetPairFirstField();
|
||||
jfieldID GetPairSecondField();
|
||||
|
||||
} // namespace IDCache
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include <android/api-level.h>
|
||||
#include <android/native_window_jni.h>
|
||||
#include <common/fs/fs.h>
|
||||
#include <core/file_sys/patch_manager.h>
|
||||
#include <core/file_sys/savedata_factory.h>
|
||||
#include <core/loader/nro.h>
|
||||
#include <jni.h>
|
||||
@ -79,6 +80,10 @@ Core::System& EmulationSession::System() {
|
||||
return m_system;
|
||||
}
|
||||
|
||||
FileSys::ManualContentProvider* EmulationSession::ContentProvider() {
|
||||
return m_manual_provider.get();
|
||||
}
|
||||
|
||||
const EmuWindow_Android& EmulationSession::Window() const {
|
||||
return *m_window;
|
||||
}
|
||||
@ -458,6 +463,17 @@ void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
|
||||
static_cast<jint>(result));
|
||||
}
|
||||
|
||||
u64 EmulationSession::getProgramId(JNIEnv* env, jstring jprogramId) {
|
||||
u64 program_id;
|
||||
auto program_id_string = GetJString(env, jprogramId);
|
||||
if (program_id_string.empty()) {
|
||||
program_id = 0;
|
||||
} else {
|
||||
program_id = std::stoull(program_id_string);
|
||||
}
|
||||
return program_id;
|
||||
}
|
||||
|
||||
static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
|
||||
MicroProfileOnThreadCreate("EmuThread");
|
||||
SCOPE_EXIT({ MicroProfileShutdown(); });
|
||||
@ -507,6 +523,27 @@ int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject
|
||||
GetJString(env, j_file_extension));
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
|
||||
jstring jprogramId,
|
||||
jstring jupdatePath) {
|
||||
u64 programId = 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& ncaDetails : item.second) {
|
||||
if (ncaDetails.second->GetName().ends_with(".cnmt.nca")) {
|
||||
auto updateId = ncaDetails.second->GetTitleId() & ~0xFFFULL;
|
||||
if (updateId == programId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz,
|
||||
jstring hook_lib_dir,
|
||||
jstring custom_driver_dir,
|
||||
@ -668,13 +705,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass
|
||||
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 j_stats = env->NewDoubleArray(4);
|
||||
|
||||
@ -699,9 +729,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass
|
||||
return ToJString(env, "JIT");
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring j_path) {}
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
|
||||
EmulationSession::GetInstance().System().ApplySettings();
|
||||
}
|
||||
|
||||
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,
|
||||
jstring j_path) {
|
||||
@ -795,4 +829,69 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
|
||||
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 programId = EmulationSession::getProgramId(env, jprogramId);
|
||||
const FileSys::PatchManager pm{programId, 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 emptyString = ToJString(env, "");
|
||||
auto emptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
|
||||
emptyString, emptyString);
|
||||
jobjectArray jaddonsArray =
|
||||
env->NewObjectArray(addons.size(), IDCache::GetPairClass(), emptyStringPair);
|
||||
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 programId = 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,
|
||||
programId, user_id->AsU128(), 0);
|
||||
return ToJString(env, user_save_data_path);
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj,
|
||||
jstring jpath) {
|
||||
EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath));
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) {
|
||||
EmulationSession::GetInstance().ContentProvider()->ClearAllEntries();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
@ -21,6 +21,7 @@ public:
|
||||
static EmulationSession& GetInstance();
|
||||
const Core::System& System() const;
|
||||
Core::System& System();
|
||||
FileSys::ManualContentProvider* ContentProvider();
|
||||
|
||||
const EmuWindow_Android& Window() const;
|
||||
EmuWindow_Android& Window();
|
||||
@ -54,6 +55,8 @@ public:
|
||||
|
||||
static void OnEmulationStarted();
|
||||
|
||||
static u64 getProgramId(JNIEnv* env, jstring jprogramId);
|
||||
|
||||
private:
|
||||
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
|
||||
static void OnEmulationStopped(Core::SystemResultStatus result);
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <common/fs/fs_util.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include "android_config.h"
|
||||
@ -12,17 +13,19 @@
|
||||
#include "frontend_common/config.h"
|
||||
#include "jni/android_common/android_common.h"
|
||||
#include "jni/id_cache.h"
|
||||
#include "native.h"
|
||||
|
||||
std::unique_ptr<AndroidConfig> config;
|
||||
std::unique_ptr<AndroidConfig> global_config;
|
||||
std::unique_ptr<AndroidConfig> per_game_config;
|
||||
|
||||
template <typename T>
|
||||
Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
|
||||
auto key = GetJString(env, jkey);
|
||||
auto basicSetting = Settings::values.linkage.by_key[key];
|
||||
auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
|
||||
if (basicSetting != 0) {
|
||||
return static_cast<Settings::Setting<T>*>(basicSetting);
|
||||
}
|
||||
auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
|
||||
if (basicAndroidSetting != 0) {
|
||||
return static_cast<Settings::Setting<T>*>(basicAndroidSetting);
|
||||
}
|
||||
@ -32,35 +35,52 @@ Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
|
||||
|
||||
extern "C" {
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeConfig(JNIEnv* env, jobject obj) {
|
||||
config = std::make_unique<AndroidConfig>();
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
|
||||
global_config = std::make_unique<AndroidConfig>();
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadConfig(JNIEnv* env, jobject obj) {
|
||||
config.reset();
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
|
||||
global_config.reset();
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadSettings(JNIEnv* env, jobject obj) {
|
||||
config->AndroidConfig::ReloadAllValues();
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) {
|
||||
global_config->AndroidConfig::ReloadAllValues();
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobject obj) {
|
||||
config->AndroidConfig::SaveAllValues();
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) {
|
||||
global_config->AndroidConfig::SaveAllValues();
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializePerGameConfig(JNIEnv* env, jobject obj,
|
||||
jstring jprogramId,
|
||||
jstring jfileName) {
|
||||
auto program_id = EmulationSession::getProgramId(env, jprogramId);
|
||||
auto file_name = GetJString(env, jfileName);
|
||||
const auto config_file_name = program_id == 0 ? file_name : fmt::format("{:016X}", program_id);
|
||||
per_game_config =
|
||||
std::make_unique<AndroidConfig>(config_file_name, Config::ConfigType::PerGameConfig);
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_isPerGameConfigLoaded(JNIEnv* env,
|
||||
jobject obj) {
|
||||
return per_game_config != nullptr;
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_savePerGameConfig(JNIEnv* env, jobject obj) {
|
||||
per_game_config->AndroidConfig::SaveAllValues();
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadPerGameConfig(JNIEnv* env, jobject obj) {
|
||||
per_game_config.reset();
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,
|
||||
jstring jkey, jboolean getDefault) {
|
||||
jstring jkey, jboolean needGlobal) {
|
||||
auto setting = getSetting<bool>(env, jkey);
|
||||
if (setting == nullptr) {
|
||||
return false;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
|
||||
if (static_cast<bool>(getDefault)) {
|
||||
return setting->GetDefault();
|
||||
}
|
||||
|
||||
return setting->GetValue();
|
||||
return setting->GetValue(static_cast<bool>(needGlobal));
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey,
|
||||
@ -69,23 +89,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject
|
||||
if (setting == nullptr) {
|
||||
return;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
setting->SetValue(static_cast<bool>(value));
|
||||
}
|
||||
|
||||
jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey,
|
||||
jboolean getDefault) {
|
||||
jboolean needGlobal) {
|
||||
auto setting = getSetting<u8>(env, jkey);
|
||||
if (setting == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
|
||||
if (static_cast<bool>(getDefault)) {
|
||||
return setting->GetDefault();
|
||||
}
|
||||
|
||||
return setting->GetValue();
|
||||
return setting->GetValue(static_cast<bool>(needGlobal));
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey,
|
||||
@ -94,23 +107,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj
|
||||
if (setting == nullptr) {
|
||||
return;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
setting->SetValue(value);
|
||||
}
|
||||
|
||||
jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey,
|
||||
jboolean getDefault) {
|
||||
jboolean needGlobal) {
|
||||
auto setting = getSetting<u16>(env, jkey);
|
||||
if (setting == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
|
||||
if (static_cast<bool>(getDefault)) {
|
||||
return setting->GetDefault();
|
||||
}
|
||||
|
||||
return setting->GetValue();
|
||||
return setting->GetValue(static_cast<bool>(needGlobal));
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey,
|
||||
@ -119,23 +125,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject ob
|
||||
if (setting == nullptr) {
|
||||
return;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
setting->SetValue(value);
|
||||
}
|
||||
|
||||
jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey,
|
||||
jboolean getDefault) {
|
||||
jboolean needGlobal) {
|
||||
auto setting = getSetting<int>(env, jkey);
|
||||
if (setting == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
|
||||
if (static_cast<bool>(getDefault)) {
|
||||
return setting->GetDefault();
|
||||
}
|
||||
|
||||
return setting->GetValue();
|
||||
return setting->GetValue(needGlobal);
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey,
|
||||
@ -144,23 +143,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj,
|
||||
if (setting == nullptr) {
|
||||
return;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
setting->SetValue(value);
|
||||
}
|
||||
|
||||
jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey,
|
||||
jboolean getDefault) {
|
||||
jboolean needGlobal) {
|
||||
auto setting = getSetting<float>(env, jkey);
|
||||
if (setting == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
|
||||
if (static_cast<bool>(getDefault)) {
|
||||
return setting->GetDefault();
|
||||
}
|
||||
|
||||
return setting->GetValue();
|
||||
return setting->GetValue(static_cast<bool>(needGlobal));
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey,
|
||||
@ -169,23 +161,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject ob
|
||||
if (setting == nullptr) {
|
||||
return;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
setting->SetValue(value);
|
||||
}
|
||||
|
||||
jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey,
|
||||
jboolean getDefault) {
|
||||
auto setting = getSetting<long>(env, jkey);
|
||||
jboolean needGlobal) {
|
||||
auto setting = getSetting<s64>(env, jkey);
|
||||
if (setting == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
|
||||
if (static_cast<bool>(getDefault)) {
|
||||
return setting->GetDefault();
|
||||
}
|
||||
|
||||
return setting->GetValue();
|
||||
return setting->GetValue(static_cast<bool>(needGlobal));
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey,
|
||||
@ -194,23 +179,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj
|
||||
if (setting == nullptr) {
|
||||
return;
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
setting->SetValue(value);
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey,
|
||||
jboolean getDefault) {
|
||||
jboolean needGlobal) {
|
||||
auto setting = getSetting<std::string>(env, jkey);
|
||||
if (setting == nullptr) {
|
||||
return ToJString(env, "");
|
||||
}
|
||||
setting->SetGlobal(true);
|
||||
|
||||
if (static_cast<bool>(getDefault)) {
|
||||
return ToJString(env, setting->GetDefault());
|
||||
}
|
||||
|
||||
return ToJString(env, setting->GetValue());
|
||||
return ToJString(env, setting->GetValue(static_cast<bool>(needGlobal)));
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey,
|
||||
@ -220,27 +198,18 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject o
|
||||
return;
|
||||
}
|
||||
|
||||
setting->SetGlobal(true);
|
||||
setting->SetValue(GetJString(env, value));
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj,
|
||||
jstring jkey) {
|
||||
auto key = GetJString(env, jkey);
|
||||
auto setting = Settings::values.linkage.by_key[key];
|
||||
if (setting != 0) {
|
||||
auto setting = getSetting<std::string>(env, jkey);
|
||||
if (setting != nullptr) {
|
||||
return setting->RuntimeModfiable();
|
||||
}
|
||||
LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
|
||||
return true;
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getConfigHeader(JNIEnv* env, jobject obj,
|
||||
jint jcategory) {
|
||||
auto category = static_cast<Settings::Category>(jcategory);
|
||||
return ToJString(env, Settings::TranslateCategory(category));
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj,
|
||||
jstring jkey) {
|
||||
auto setting = getSetting<std::string>(env, jkey);
|
||||
@ -254,6 +223,41 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e
|
||||
return ToJString(env, setting->PairedSetting()->GetLabel());
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSwitchable(JNIEnv* env, jobject obj,
|
||||
jstring jkey) {
|
||||
auto setting = getSetting<std::string>(env, jkey);
|
||||
if (setting != nullptr) {
|
||||
return setting->Switchable();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_usingGlobal(JNIEnv* env, jobject obj,
|
||||
jstring jkey) {
|
||||
auto setting = getSetting<std::string>(env, jkey);
|
||||
if (setting != nullptr) {
|
||||
return setting->UsingGlobal();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGlobal(JNIEnv* env, jobject obj, jstring jkey,
|
||||
jboolean global) {
|
||||
auto setting = getSetting<std::string>(env, jkey);
|
||||
if (setting != nullptr) {
|
||||
setting->SetGlobal(static_cast<bool>(global));
|
||||
}
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDefaultToString(JNIEnv* env, jobject obj,
|
||||
jstring jkey) {
|
||||
auto setting = getSetting<std::string>(env, jkey);
|
||||
if (setting != nullptr) {
|
||||
return ToJString(env, setting->DefaultToString());
|
||||
}
|
||||
return ToJString(env, "");
|
||||
}
|
||||
|
||||
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) {
|
||||
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||
jmethodID gameDirConstructor = IDCache::GetGameDirConstructor();
|
||||
@ -305,4 +309,30 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject
|
||||
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"
|
||||
|
@ -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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="center">
|
||||
|
||||
|
@ -16,7 +16,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="center"
|
||||
android:padding="24dp">
|
||||
android:paddingVertical="16dp"
|
||||
android:paddingHorizontal="24dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
@ -50,6 +51,23 @@
|
||||
android:textAlignment="viewStart"
|
||||
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>
|
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>
|
@ -62,6 +62,16 @@
|
||||
android:textSize="13sp"
|
||||
tools:text="1x" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_clear"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
android:text="@string/clear"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -10,22 +10,23 @@
|
||||
android:minHeight="72dp"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_widget"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_toStartOf="@+id/switch_widget"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="1">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_setting_name"
|
||||
@ -48,4 +49,24 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_widget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_clear"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/clear"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -11,6 +11,11 @@
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/preferences_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_settings_per_game"
|
||||
android:icon="@drawable/ic_settings_outline"
|
||||
android:title="@string/per_game_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_overlay_controls"
|
||||
android:icon="@drawable/ic_controller"
|
||||
|
@ -15,6 +15,10 @@
|
||||
app:argType="org.yuzu.yuzu_emu.model.Game"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null" />
|
||||
<argument
|
||||
android:name="custom"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
|
@ -77,6 +77,10 @@
|
||||
app:argType="org.yuzu.yuzu_emu.model.Game"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null" />
|
||||
<argument
|
||||
android:name="custom"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</activity>
|
||||
|
||||
<action
|
||||
@ -107,7 +111,13 @@
|
||||
<fragment
|
||||
android:id="@+id/driverManagerFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
|
||||
android:label="DriverManagerFragment" />
|
||||
android:label="DriverManagerFragment" >
|
||||
<argument
|
||||
android:name="game"
|
||||
app:argType="org.yuzu.yuzu_emu.model.Game"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/appletLauncherFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment"
|
||||
@ -124,5 +134,41 @@
|
||||
android:id="@+id/gameFoldersFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.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" />
|
||||
<action
|
||||
android:id="@+id/action_perGamePropertiesFragment_to_driverManagerFragment"
|
||||
app:destination="@id/driverManagerFragment" />
|
||||
</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>
|
||||
|
@ -13,7 +13,7 @@
|
||||
<dimen name="menu_width">256dp</dimen>
|
||||
<dimen name="card_width">165dp</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="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="manage_save_data">Manage save data</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="save_files_importing">Importing 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_invalid_zip_structure">Invalid save directory structure</string>
|
||||
@ -266,6 +269,11 @@
|
||||
<string name="delete">Delete</string>
|
||||
<string name="edit">Edit</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 -->
|
||||
<string name="select_gpu_driver">Select GPU driver</string>
|
||||
@ -291,6 +299,43 @@
|
||||
<string name="preferences_debug">Debug</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 anyways?</string>
|
||||
|
||||
<!-- ROM loading errors -->
|
||||
<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>
|
||||
@ -369,6 +414,7 @@
|
||||
|
||||
<!-- Memory Sizes -->
|
||||
<string name="memory_byte">Byte</string>
|
||||
<string name="memory_byte_shorthand">B</string>
|
||||
<string name="memory_kilobyte">KB</string>
|
||||
<string name="memory_megabyte">MB</string>
|
||||
<string name="memory_gigabyte">GB</string>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user