early-access version 4023

This commit is contained in:
pineappleEA 2023-12-16 21:54:30 +01:00
parent 2a679302ce
commit 16855a5c39
114 changed files with 3587 additions and 890 deletions

View File

@ -1,7 +1,7 @@
yuzu emulator early access yuzu emulator early access
============= =============
This is the source code for early-access 4022. This is the source code for early-access 4023.
## Legal Notice ## Legal Notice

View File

@ -230,8 +230,6 @@ object NativeLibrary {
*/ */
external fun onTouchReleased(finger_id: Int) external fun onTouchReleased(finger_id: Int)
external fun initGameIni(gameID: String?)
external fun setAppDirectory(directory: String) external fun setAppDirectory(directory: String)
/** /**
@ -241,6 +239,8 @@ object NativeLibrary {
*/ */
external fun installFileToNand(filename: String, extension: String): Int external fun installFileToNand(filename: String, extension: String): Int
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
external fun initializeGpuDriver( external fun initializeGpuDriver(
hookLibDir: String?, hookLibDir: String?,
customDriverDir: String?, customDriverDir: String?,
@ -252,18 +252,11 @@ object NativeLibrary {
external fun initializeSystem(reload: Boolean) external fun initializeSystem(reload: Boolean)
external fun defaultCPUCore(): Int
/** /**
* Begins emulation. * Begins emulation.
*/ */
external fun run(path: String?) external fun run(path: String?)
/**
* Begins emulation from the specified savestate.
*/
external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
// Surface Handling // Surface Handling
external fun surfaceChanged(surf: Surface?) external fun surfaceChanged(surf: Surface?)
@ -304,10 +297,9 @@ object NativeLibrary {
*/ */
external fun getCpuBackend(): String external fun getCpuBackend(): String
/** external fun applySettings()
* Notifies the core emulation that the orientation has changed.
*/ external fun logSettings()
external fun notifyOrientationChange(layout_option: Int, rotation: Int)
enum class CoreError { enum class CoreError {
ErrorSystemFiles, ErrorSystemFiles,
@ -538,6 +530,35 @@ object NativeLibrary {
*/ */
external fun isFirmwareAvailable(): Boolean external fun isFirmwareAvailable(): Boolean
/**
* Checks the PatchManager for any addons that are available
*
* @param path Path to game file. Can be a [Uri].
* @param programId String representation of a game's program ID
* @return Array of pairs where the first value is the name of an addon and the second is the version
*/
external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
/**
* Gets the save location for a specific game
*
* @param programId String representation of a game's program ID
* @return Save data path that may not exist yet
*/
external fun getSavePath(programId: String): String
/**
* 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 * Button type for use in onTouchEvent
*/ */

View File

@ -172,7 +172,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
override fun onUserLeaveHint() { override fun onUserLeaveHint() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { 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() val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
enterPictureInPictureMode(pictureInPictureParamsBuilder.build()) enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
@ -284,7 +284,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
PictureInPictureParams.Builder { PictureInPictureParams.Builder {
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) { val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) {
0 -> Rational(16, 9) 0 -> Rational(16, 9)
1 -> Rational(4, 3) 1 -> Rational(4, 3)
2 -> Rational(21, 9) 2 -> Rational(21, 9)
@ -331,7 +331,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
pictureInPictureActions.add(pauseRemoteAction) pictureInPictureActions.add(pauseRemoteAction)
} }
if (BooleanSetting.AUDIO_MUTED.boolean) { if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
val unmuteIcon = Icon.createWithResource( val unmuteIcon = Icon.createWithResource(
this@EmulationActivity, this@EmulationActivity,
R.drawable.ic_pip_unmute R.drawable.ic_pip_unmute
@ -376,7 +376,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
val isEmulationActive = emulationViewModel.emulationStarted.value && val isEmulationActive = emulationViewModel.emulationStarted.value &&
!emulationViewModel.isEmulationStopping.value !emulationViewModel.isEmulationStopping.value
pictureInPictureParamsBuilder.setAutoEnterEnabled( pictureInPictureParamsBuilder.setAutoEnterEnabled(
BooleanSetting.PICTURE_IN_PICTURE.boolean && isEmulationActive BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive
) )
} }
setPictureInPictureParams(pictureInPictureParamsBuilder.build()) setPictureInPictureParams(pictureInPictureParamsBuilder.build())
@ -390,9 +390,13 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation() if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
} }
if (intent.action == actionUnmute) { 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) { } 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() buildPictureInPictureParams()
} }
@ -423,7 +427,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
// Always resume audio, since there is no UI button // 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)
}
} }
} }

View File

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

View File

@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
import org.yuzu.yuzu_emu.model.Applet import org.yuzu.yuzu_emu.model.Applet
import org.yuzu.yuzu_emu.model.AppletInfo import org.yuzu.yuzu_emu.model.AppletInfo
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): AppletAdapter.AppletViewHolder { ): AppletAdapter.AppletViewHolder {
CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.apply { root.setOnClickListener(this@AppletAdapter) } .apply { root.setOnClickListener(this@AppletAdapter) }
.also { return AppletViewHolder(it) } .also { return AppletViewHolder(it) }
} }
@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
view.findNavController().navigate(action) view.findNavController().navigate(action)
} }
inner class AppletViewHolder(val binding: CardAppletOptionBinding) : inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
lateinit var applet: Applet lateinit var applet: Applet

View File

@ -42,7 +42,7 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
if (driverViewModel.selectedDriver > position) { if (driverViewModel.selectedDriver > position) {
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1) driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
} }
if (GpuDriverHelper.customDriverData == driverData.second) { if (GpuDriverHelper.customDriverSettingData == driverData.second) {
driverViewModel.setSelectedDriverIndex(0) driverViewModel.setSelectedDriverIndex(0)
} }
driverViewModel.driversToDelete.add(driverData.first) driverViewModel.driversToDelete.add(driverData.first)

View File

@ -44,19 +44,20 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
class GameAdapter(private val activity: AppCompatActivity) : class GameAdapter(private val activity: AppCompatActivity) :
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
View.OnClickListener { View.OnClickListener,
View.OnLongClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
// Create a new view. // Create a new view.
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
binding.cardGame.setOnClickListener(this) binding.cardGame.setOnClickListener(this)
binding.cardGame.setOnLongClickListener(this)
// Use that view to create a ViewHolder. // Use that view to create a ViewHolder.
return GameViewHolder(binding) return GameViewHolder(binding)
} }
override fun onBindViewHolder(holder: GameViewHolder, position: Int) { override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
holder.bind(currentList[position]) holder.bind(currentList[position])
}
override fun getItemCount(): Int = currentList.size override fun getItemCount(): Int = currentList.size
@ -125,10 +126,17 @@ class GameAdapter(private val activity: AppCompatActivity) :
} }
} }
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game) val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
view.findNavController().navigate(action) view.findNavController().navigate(action)
} }
override fun onLongClick(view: View): Boolean {
val holder = view.tag as GameViewHolder
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
view.findNavController().navigate(action)
return true
}
inner class GameViewHolder(val binding: CardGameBinding) : inner class GameViewHolder(val binding: CardGameBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
lateinit var game: Game lateinit var game: Game
@ -157,7 +165,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
private class DiffCallback : DiffUtil.ItemCallback<Game>() { private class DiffCallback : DiffUtil.ItemCallback<Game>() {
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
return oldItem.programId == newItem.programId return oldItem == newItem
} }
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {

View File

@ -0,0 +1,140 @@
// 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.CardInstallableIconBinding
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(
CardInstallableIconBinding.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: CardInstallableIconBinding) :
GamePropertyViewHolder(binding.root) {
override fun bind(property: GameProperty) {
val installableProperty = property as InstallableProperty
binding.title.setText(installableProperty.titleId)
binding.description.setText(installableProperty.descriptionId)
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.context.resources,
installableProperty.iconId,
binding.icon.context.theme
)
)
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
}
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractBooleanSetting : AbstractSetting { interface AbstractBooleanSetting : AbstractSetting {
val boolean: Boolean fun getBoolean(needsGlobal: Boolean = false): Boolean
fun setBoolean(value: Boolean) fun setBoolean(value: Boolean)
} }

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractByteSetting : AbstractSetting { interface AbstractByteSetting : AbstractSetting {
val byte: Byte fun getByte(needsGlobal: Boolean = false): Byte
fun setByte(value: Byte) fun setByte(value: Byte)
} }

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractFloatSetting : AbstractSetting { interface AbstractFloatSetting : AbstractSetting {
val float: Float fun getFloat(needsGlobal: Boolean = false): Float
fun setFloat(value: Float) fun setFloat(value: Float)
} }

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractIntSetting : AbstractSetting { interface AbstractIntSetting : AbstractSetting {
val int: Int fun getInt(needsGlobal: Boolean = false): Int
fun setInt(value: Int) fun setInt(value: Int)
} }

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractLongSetting : AbstractSetting { interface AbstractLongSetting : AbstractSetting {
val long: Long fun getLong(needsGlobal: Boolean = false): Long
fun setLong(value: Long) fun setLong(value: Long)
} }

View File

@ -7,12 +7,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
interface AbstractSetting { interface AbstractSetting {
val key: String val key: String
val category: Settings.Category
val defaultValue: Any val defaultValue: Any
val androidDefault: Any?
get() = null
val valueAsString: String
get() = ""
val isRuntimeModifiable: Boolean val isRuntimeModifiable: Boolean
get() = NativeConfig.getIsRuntimeModifiable(key) get() = NativeConfig.getIsRuntimeModifiable(key)
@ -20,5 +15,17 @@ interface AbstractSetting {
val pairedSettingKey: String val pairedSettingKey: String
get() = NativeConfig.getPairedSettingKey(key) get() = NativeConfig.getPairedSettingKey(key)
val isSwitchable: Boolean
get() = NativeConfig.getIsSwitchable(key)
var global: Boolean
get() = NativeConfig.usingGlobal(key)
set(value) = NativeConfig.setGlobal(key, value)
val isSaveable: Boolean
get() = NativeConfig.getIsSaveable(key)
fun getValueAsString(needsGlobal: Boolean = false): String
fun reset() fun reset()
} }

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractShortSetting : AbstractSetting { interface AbstractShortSetting : AbstractSetting {
val short: Short fun getShort(needsGlobal: Boolean = false): Short
fun setShort(value: Short) fun setShort(value: Short)
} }

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractStringSetting : AbstractSetting { interface AbstractStringSetting : AbstractSetting {
val string: String fun getString(needsGlobal: Boolean = false): String
fun setString(value: String) fun setString(value: String)
} }

View File

@ -5,36 +5,34 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
enum class BooleanSetting( enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
override val key: String, AUDIO_MUTED("audio_muted"),
override val category: Settings.Category, CPU_DEBUG_MODE("cpu_debug_mode"),
override val androidDefault: Boolean? = null FASTMEM("cpuopt_fastmem"),
) : AbstractBooleanSetting { FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
AUDIO_MUTED("audio_muted", Settings.Category.Audio), RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu), USE_DOCKED_MODE("use_docked_mode"),
FASTMEM("cpuopt_fastmem", Settings.Category.Cpu), RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"),
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu), RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core), RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false), RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer), RENDERER_DEBUG("debug"),
RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer), PICTURE_IN_PICTURE("picture_in_picture"),
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer), USE_CUSTOM_RTC("custom_rtc_enabled");
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);
override val boolean: Boolean override fun getBoolean(needsGlobal: Boolean): Boolean =
get() = NativeConfig.getBoolean(key, false) NativeConfig.getBoolean(key, needsGlobal)
override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value) override fun setBoolean(value: Boolean) {
if (NativeConfig.isPerGameConfigLoaded()) {
override val defaultValue: Boolean by lazy { global = false
androidDefault ?: NativeConfig.getBoolean(key, true) }
NativeConfig.setBoolean(key, value)
} }
override val valueAsString: String override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() }
get() = if (boolean) "1" else "0"
override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString()
override fun reset() = NativeConfig.setBoolean(key, defaultValue) override fun reset() = NativeConfig.setBoolean(key, defaultValue)
} }

View File

@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ByteSetting( enum class ByteSetting(override val key: String) : AbstractByteSetting {
override val key: String, AUDIO_VOLUME("volume");
override val category: Settings.Category
) : AbstractByteSetting {
AUDIO_VOLUME("volume", Settings.Category.Audio);
override val byte: Byte override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
get() = NativeConfig.getByte(key, false)
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 override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString()
get() = byte.toString()
override fun reset() = NativeConfig.setByte(key, defaultValue) override fun reset() = NativeConfig.setByte(key, defaultValue)
} }

View File

@ -5,22 +5,22 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
enum class FloatSetting( enum class FloatSetting(override val key: String) : AbstractFloatSetting {
override val key: String,
override val category: Settings.Category
) : AbstractFloatSetting {
// No float settings currently exist // No float settings currently exist
EMPTY_SETTING("", Settings.Category.UiGeneral); EMPTY_SETTING("");
override val float: Float override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false)
get() = 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 override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString()
get() = float.toString()
override fun reset() = NativeConfig.setFloat(key, defaultValue) override fun reset() = NativeConfig.setFloat(key, defaultValue)
} }

View File

@ -5,36 +5,33 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
enum class IntSetting( enum class IntSetting(override val key: String) : AbstractIntSetting {
override val key: String, CPU_BACKEND("cpu_backend"),
override val category: Settings.Category, CPU_ACCURACY("cpu_accuracy"),
override val androidDefault: Int? = null REGION_INDEX("region_index"),
) : AbstractIntSetting { LANGUAGE_INDEX("language_index"),
CPU_BACKEND("cpu_backend", Settings.Category.Cpu), RENDERER_BACKEND("backend"),
CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu), RENDERER_ACCURACY("gpu_accuracy"),
REGION_INDEX("region_index", Settings.Category.System), RENDERER_RESOLUTION("resolution_setup"),
LANGUAGE_INDEX("language_index", Settings.Category.System), RENDERER_VSYNC("use_vsync"),
RENDERER_BACKEND("backend", Settings.Category.Renderer), RENDERER_SCALING_FILTER("scaling_filter"),
RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0), RENDERER_ANTI_ALIASING("anti_aliasing"),
RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer), RENDERER_SCREEN_LAYOUT("screen_layout"),
RENDERER_VSYNC("use_vsync", Settings.Category.Renderer), RENDERER_ASPECT_RATIO("aspect_ratio"),
RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer), AUDIO_OUTPUT_ENGINE("output_engine");
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);
override val int: Int override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
get() = NativeConfig.getInt(key, false)
override fun setInt(value: Int) = NativeConfig.setInt(key, value) override fun setInt(value: Int) {
if (NativeConfig.isPerGameConfigLoaded()) {
override val defaultValue: Int by lazy { global = false
androidDefault ?: NativeConfig.getInt(key, true) }
NativeConfig.setInt(key, value)
} }
override val valueAsString: String override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() }
get() = int.toString()
override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString()
override fun reset() = NativeConfig.setInt(key, defaultValue) override fun reset() = NativeConfig.setInt(key, defaultValue)
} }

View File

@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
enum class LongSetting( enum class LongSetting(override val key: String) : AbstractLongSetting {
override val key: String, CUSTOM_RTC("custom_rtc");
override val category: Settings.Category
) : AbstractLongSetting {
CUSTOM_RTC("custom_rtc", Settings.Category.System);
override val long: Long override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal)
get() = NativeConfig.getLong(key, false)
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 override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString()
get() = long.toString()
override fun reset() = NativeConfig.setLong(key, defaultValue) override fun reset() = NativeConfig.setLong(key, defaultValue)
} }

View File

@ -6,62 +6,11 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
object Settings { 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) { enum class MenuTag(val titleId: Int) {
SECTION_ROOT(R.string.advanced_settings), SECTION_ROOT(R.string.advanced_settings),
SECTION_SYSTEM(R.string.preferences_system), SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics), SECTION_RENDERER(R.string.preferences_graphics),
SECTION_AUDIO(R.string.preferences_audio), SECTION_AUDIO(R.string.preferences_audio),
SECTION_CPU(R.string.cpu),
SECTION_THEME(R.string.preferences_theme), SECTION_THEME(R.string.preferences_theme),
SECTION_DEBUG(R.string.preferences_debug); SECTION_DEBUG(R.string.preferences_debug);
} }

View File

@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ShortSetting( enum class ShortSetting(override val key: String) : AbstractShortSetting {
override val key: String, RENDERER_SPEED_LIMIT("speed_limit");
override val category: Settings.Category
) : AbstractShortSetting {
RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
override val short: Short override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
get() = NativeConfig.getShort(key, false)
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 override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
get() = short.toString()
override fun reset() = NativeConfig.setShort(key, defaultValue) override fun reset() = NativeConfig.setShort(key, defaultValue)
} }

View File

@ -5,22 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
enum class StringSetting( enum class StringSetting(override val key: String) : AbstractStringSetting {
override val key: String, DRIVER_PATH("driver_path");
override val category: Settings.Category
) : AbstractStringSetting {
// No string settings currently exist
EMPTY_SETTING("", Settings.Category.UiGeneral);
override val string: String override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
get() = NativeConfig.getString(key, false)
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 override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal)
get() = string
override fun reset() = NativeConfig.setString(key, defaultValue) override fun reset() = NativeConfig.setString(key, defaultValue)
} }

View File

@ -12,7 +12,6 @@ class DateTimeSetting(
) : SettingsItem(longSetting, titleId, descriptionId) { ) : SettingsItem(longSetting, titleId, descriptionId) {
override val type = TYPE_DATETIME_SETTING override val type = TYPE_DATETIME_SETTING
var value: Long fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
get() = longSetting.long fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value)
set(value) = (setting as AbstractLongSetting).setLong(value)
} }

View File

@ -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.ByteSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting 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.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting 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. * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
@ -30,10 +30,26 @@ abstract class SettingsItem(
val isEditable: Boolean val isEditable: Boolean
get() { get() {
// Can't edit settings that aren't saveable in per-game config even if they are switchable
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false
}
if (!NativeLibrary.isRunning()) return true 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 return setting.isRuntimeModifiable
} }
val needsRuntimeGlobal: Boolean
get() = NativeLibrary.isRunning() && !setting.global &&
!NativeConfig.isPerGameConfigLoaded()
companion object { companion object {
const val TYPE_HEADER = 0 const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1 const val TYPE_SWITCH = 1
@ -48,8 +64,9 @@ abstract class SettingsItem(
val emptySetting = object : AbstractSetting { val emptySetting = object : AbstractSetting {
override val key: String = "" override val key: String = ""
override val category: Settings.Category = Settings.Category.Ui
override val defaultValue: Any = false override val defaultValue: Any = false
override val isSaveable = true
override fun getValueAsString(needsGlobal: Boolean): String = ""
override fun reset() {} override fun reset() {}
} }
@ -270,9 +287,9 @@ abstract class SettingsItem(
) )
val fastmem = object : AbstractBooleanSetting { val fastmem = object : AbstractBooleanSetting {
override val boolean: Boolean override fun getBoolean(needsGlobal: Boolean): Boolean =
get() = BooleanSetting.FASTMEM.getBoolean() &&
BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean()
override fun setBoolean(value: Boolean) { override fun setBoolean(value: Boolean) {
BooleanSetting.FASTMEM.setBoolean(value) BooleanSetting.FASTMEM.setBoolean(value)
@ -280,9 +297,24 @@ abstract class SettingsItem(
} }
override val key: String = FASTMEM_COMBINED override val key: String = FASTMEM_COMBINED
override val category = Settings.Category.Cpu
override val isRuntimeModifiable: Boolean = false override val isRuntimeModifiable: Boolean = false
override val defaultValue: Boolean = true 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 val isSaveable = true
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean().toString()
override fun reset() = setBoolean(defaultValue) override fun reset() = setBoolean(defaultValue)
} }
put(SwitchSetting(fastmem, R.string.fastmem, 0)) put(SwitchSetting(fastmem, R.string.fastmem, 0))

View File

@ -15,16 +15,11 @@ class SingleChoiceSetting(
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SINGLE_CHOICE override val type = TYPE_SINGLE_CHOICE
var selectedValue: Int fun getSelectedValue(needsGlobal: Boolean = false) =
get() { when (setting) {
return when (setting) { is AbstractIntSetting -> setting.getInt(needsGlobal)
is AbstractIntSetting -> setting.int
else -> -1 else -> -1
} }
}
set(value) { fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value)
when (setting) {
is AbstractIntSetting -> setting.setInt(value)
}
}
} }

View File

@ -20,22 +20,20 @@ class SliderSetting(
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SLIDER override val type = TYPE_SLIDER
var selectedValue: Int fun getSelectedValue(needsGlobal: Boolean = false) =
get() { when (setting) {
return when (setting) { is AbstractByteSetting -> setting.getByte(needsGlobal).toInt()
is AbstractByteSetting -> setting.byte.toInt() is AbstractShortSetting -> setting.getShort(needsGlobal).toInt()
is AbstractShortSetting -> setting.short.toInt() is AbstractIntSetting -> setting.getInt(needsGlobal)
is AbstractIntSetting -> setting.int is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt()
is AbstractFloatSetting -> setting.float.roundToInt()
else -> -1 else -> -1
} }
}
set(value) { fun setSelectedValue(value: Int) =
when (setting) { when (setting) {
is AbstractByteSetting -> setting.setByte(value.toByte()) is AbstractByteSetting -> setting.setByte(value.toByte())
is AbstractShortSetting -> setting.setShort(value.toShort()) is AbstractShortSetting -> setting.setShort(value.toShort())
is AbstractIntSetting -> setting.setInt(value)
is AbstractFloatSetting -> setting.setFloat(value.toFloat()) is AbstractFloatSetting -> setting.setFloat(value.toFloat())
} else -> (setting as AbstractIntSetting).setInt(value)
} }
} }

View File

@ -17,14 +17,13 @@ class StringSingleChoiceSetting(
fun getValueAt(index: Int): String = fun getValueAt(index: Int): String =
if (index >= 0 && index < values.size) values[index] else "" if (index >= 0 && index < values.size) values[index] else ""
var selectedValue: String fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
get() = stringSetting.string fun setSelectedValue(value: String) = stringSetting.setString(value)
set(value) = stringSetting.setString(value)
val selectValueIndex: Int val selectValueIndex: Int
get() { get() {
for (i in values.indices) { for (i in values.indices) {
if (values[i] == selectedValue) { if (values[i] == getSelectedValue()) {
return i return i
} }
} }

View File

@ -14,15 +14,15 @@ class SwitchSetting(
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SWITCH override val type = TYPE_SWITCH
var checked: Boolean fun getIsChecked(needsGlobal: Boolean = false): Boolean {
get() {
return when (setting) { return when (setting) {
is AbstractIntSetting -> setting.int == 1 is AbstractIntSetting -> setting.getInt(needsGlobal) == 1
is AbstractBooleanSetting -> setting.boolean is AbstractBooleanSetting -> setting.getBoolean(needsGlobal)
else -> false else -> false
} }
} }
set(value) {
fun setChecked(value: Boolean) {
when (setting) { when (setting) {
is AbstractIntSetting -> setting.setInt(if (value) 1 else 0) is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
is AbstractBooleanSetting -> setting.setBoolean(value) is AbstractBooleanSetting -> setting.setBoolean(value)

View File

@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.NativeLibrary
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() {
binding = ActivitySettingsBinding.inflate(layoutInflater) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
SettingsFile.loadCustomConfig(args.game!!)
}
settingsViewModel.game = args.game settingsViewModel.game = args.game
val navHostFragment = val navHostFragment =
@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// TODO: Load custom settings contextually
if (!DirectoryInitialization.areDirectoriesReady) { if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start() DirectoryInitialization.start()
} }
@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
CoroutineScope(Dispatchers.IO).launch { Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
NativeConfig.saveSettings() 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() { fun onSettingsReset() {
// Delete settings file because the user may have changed values that do not exist in the UI // 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) val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
if (!settingsFile.delete()) { if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile") 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( Toast.makeText(
applicationContext, applicationContext,

View File

@ -102,8 +102,9 @@ class SettingsAdapter(
return currentList[position].type return currentList[position].type
} }
fun onBooleanClick(item: SwitchSetting, checked: Boolean) { fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) {
item.checked = checked item.setChecked(checked)
notifyItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true) settingsViewModel.setShouldReloadSettingsList(true)
} }
@ -126,7 +127,7 @@ class SettingsAdapter(
} }
fun onDateTimeClick(item: DateTimeSetting, position: Int) { 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 // Helper to extract hour and minute from epoch time
val calendar: Calendar = Calendar.getInstance() val calendar: Calendar = Calendar.getInstance()
@ -159,9 +160,9 @@ class SettingsAdapter(
var epochTime: Long = datePicker.selection!! / 1000 var epochTime: Long = datePicker.selection!! / 1000
epochTime += timePicker.hour.toLong() * 60 * 60 epochTime += timePicker.hour.toLong() * 60 * 60
epochTime += timePicker.minute.toLong() * 60 epochTime += timePicker.minute.toLong() * 60
if (item.value != epochTime) { if (item.getValue() != epochTime) {
notifyItemChanged(position) notifyItemChanged(position)
item.value = epochTime item.setValue(epochTime)
} }
} }
datePicker.show( datePicker.show(
@ -195,6 +196,12 @@ class SettingsAdapter(
return true return true
} }
fun onClearClick(item: SettingsItem, position: Int) {
item.setting.global = true
notifyItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
}
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
return oldItem.setting.key == newItem.setting.key return oldItem.setting.key == newItem.setting.key

View File

@ -66,7 +66,13 @@ class SettingsFragment : Fragment() {
args.menuTag 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 { binding.listSettings.apply {
adapter = settingsAdapter adapter = settingsAdapter
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())

View File

@ -7,6 +7,7 @@ import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
@ -31,12 +32,27 @@ class SettingsFragmentPresenter(
private val preferences: SharedPreferences private val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 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) { fun ArrayList<SettingsItem>.add(key: String) {
val item = SettingsItem.settingsItems[key]!! 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 val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) { 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 if (!pairedSettingValue) return
} }
add(item) add(item)
@ -153,8 +169,8 @@ class SettingsFragmentPresenter(
private fun addThemeSettings(sl: ArrayList<SettingsItem>) { private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
sl.apply { sl.apply {
val theme: AbstractIntSetting = object : AbstractIntSetting { val theme: AbstractIntSetting = object : AbstractIntSetting {
override val int: Int override fun getInt(needsGlobal: Boolean): Int =
get() = preferences.getInt(Settings.PREF_THEME, 0) preferences.getInt(Settings.PREF_THEME, 0)
override fun setInt(value: Int) { override fun setInt(value: Int) {
preferences.edit() preferences.edit()
@ -164,8 +180,8 @@ class SettingsFragmentPresenter(
} }
override val key: String = Settings.PREF_THEME override val key: String = Settings.PREF_THEME
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false override val isRuntimeModifiable: Boolean = false
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override val defaultValue: Int = 0 override val defaultValue: Int = 0
override fun reset() { override fun reset() {
preferences.edit() preferences.edit()
@ -197,8 +213,8 @@ class SettingsFragmentPresenter(
} }
val themeMode: AbstractIntSetting = object : AbstractIntSetting { val themeMode: AbstractIntSetting = object : AbstractIntSetting {
override val int: Int override fun getInt(needsGlobal: Boolean): Int =
get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) preferences.getInt(Settings.PREF_THEME_MODE, -1)
override fun setInt(value: Int) { override fun setInt(value: Int) {
preferences.edit() preferences.edit()
@ -208,8 +224,8 @@ class SettingsFragmentPresenter(
} }
override val key: String = Settings.PREF_THEME_MODE override val key: String = Settings.PREF_THEME_MODE
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false override val isRuntimeModifiable: Boolean = false
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override val defaultValue: Int = -1 override val defaultValue: Int = -1
override fun reset() { override fun reset() {
preferences.edit() preferences.edit()
@ -230,8 +246,8 @@ class SettingsFragmentPresenter(
) )
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
override val boolean: Boolean override fun getBoolean(needsGlobal: Boolean): Boolean =
get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
override fun setBoolean(value: Boolean) { override fun setBoolean(value: Boolean) {
preferences.edit() preferences.edit()
@ -241,8 +257,10 @@ class SettingsFragmentPresenter(
} }
override val key: String = Settings.PREF_BLACK_BACKGROUNDS override val key: String = Settings.PREF_BLACK_BACKGROUNDS
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false override val isRuntimeModifiable: Boolean = false
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean().toString()
override val defaultValue: Boolean = false override val defaultValue: Boolean = false
override fun reset() { override fun reset() {
preferences.edit() preferences.edit()

View File

@ -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.DateTimeSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -29,12 +30,23 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
} }
binding.textSettingValue.visibility = View.VISIBLE binding.textSettingValue.visibility = View.VISIBLE
val epochTime = setting.value val epochTime = setting.getValue()
val instant = Instant.ofEpochMilli(epochTime * 1000) val instant = Instant.ofEpochMilli(epochTime * 1000)
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
binding.textSettingValue.text = dateFormatter.format(zonedTime) 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) setStyle(setting.isEditable, binding)
} }

View File

@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.GONE binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
setStyle(setting.isEditable, binding) setStyle(setting.isEditable, binding)
} }

View File

@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
binding.textSettingName.alpha = opacity binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity binding.textSettingDescription.alpha = opacity
binding.textSettingValue.alpha = opacity binding.textSettingValue.alpha = opacity
binding.buttonClear.isEnabled = isEditable
} }
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) { 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 val opacity = if (isEditable) 1.0f else 0.5f
binding.textSettingName.alpha = opacity binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity binding.textSettingDescription.alpha = opacity
binding.buttonClear.isEnabled = isEditable
} }
} }

View File

@ -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.SingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -29,20 +30,31 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
val resMgr = binding.textSettingValue.context.resources val resMgr = binding.textSettingValue.context.resources
val values = resMgr.getIntArray(item.valuesId) val values = resMgr.getIntArray(item.valuesId)
for (i in values.indices) { for (i in values.indices) {
if (values[i] == item.selectedValue) { if (values[i] == item.getSelectedValue()) {
binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
break break
} }
} }
} else if (item is StringSingleChoiceSetting) { } else if (item is StringSingleChoiceSetting) {
for (i in item.values.indices) { for (i in item.values.indices) {
if (item.values[i] == item.selectedValue) { if (item.values[i] == item.getSelectedValue()) {
binding.textSettingValue.text = item.choices[i] binding.textSettingValue.text = item.choices[i]
break 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) setStyle(setting.isEditable, binding)
} }

View File

@ -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.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -26,10 +27,21 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
binding.textSettingValue.visibility = View.VISIBLE binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingValue.text = String.format( binding.textSettingValue.text = String.format(
binding.textSettingValue.context.getString(R.string.value_with_units), binding.textSettingValue.context.getString(R.string.value_with_units),
setting.selectedValue, setting.getSelectedValue(),
setting.units 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) setStyle(setting.isEditable, binding)
} }

View File

@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.GONE binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {

View File

@ -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.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -27,9 +28,20 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
} }
binding.switchWidget.setOnCheckedChangeListener(null) binding.switchWidget.setOnCheckedChangeListener(null)
binding.switchWidget.isChecked = setting.checked binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> 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) setStyle(setting.isEditable, binding)

View File

@ -3,15 +3,27 @@
package org.yuzu.yuzu_emu.features.settings.utils package org.yuzu.yuzu_emu.features.settings.utils
import android.net.Uri
import org.yuzu.yuzu_emu.model.Game
import java.io.* import java.io.*
import org.yuzu.yuzu_emu.utils.DirectoryInitialization 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. * Contains static methods for interacting with .ini files in which settings are stored.
*/ */
object SettingsFile { object SettingsFile {
const val FILE_NAME_CONFIG = "config" const val FILE_NAME_CONFIG = "config.ini"
fun getSettingsFile(fileName: String): File = 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)
}
} }

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -36,6 +37,8 @@ class DriverManagerFragment : Fragment() {
private val homeViewModel: HomeViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels()
private val driverViewModel: DriverViewModel by activityViewModels() private val driverViewModel: DriverViewModel by activityViewModels()
private val args by navArgs<DriverManagerFragmentArgs>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -57,7 +60,9 @@ class DriverManagerFragment : Fragment() {
homeViewModel.setNavigationVisibility(visible = false, animated = true) homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) homeViewModel.setStatusBarShadeVisibility(visible = false)
if (!driverViewModel.isInteractionAllowed) { driverViewModel.onOpenDriverManager(args.game)
if (!driverViewModel.isInteractionAllowed.value) {
DriversLoadingDialogFragment().show( DriversLoadingDialogFragment().show(
childFragmentManager, childFragmentManager,
DriversLoadingDialogFragment.TAG DriversLoadingDialogFragment.TAG
@ -102,10 +107,9 @@ class DriverManagerFragment : Fragment() {
setInsets() setInsets()
} }
// Start installing requested driver override fun onDestroy() {
override fun onStop() { super.onDestroy()
super.onStop() driverViewModel.onCloseDriverManager(args.game)
driverViewModel.onCloseDriverManager()
} }
private fun setInsets() = private fun setInsets() =

View File

@ -47,25 +47,9 @@ class DriversLoadingDialogFragment : DialogFragment() {
viewLifecycleOwner.lifecycleScope.apply { viewLifecycleOwner.lifecycleScope.apply {
launch { launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { 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()
} }
} }

View File

@ -52,6 +52,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.IntSetting 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.model.Settings
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.EmulationViewModel import org.yuzu.yuzu_emu.model.EmulationViewModel
@ -127,6 +128,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
return return
} }
// Always load custom settings when launching a game from an intent
if (args.custom || intentGame != null) {
SettingsFile.loadCustomConfig(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. // So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true retainInstance = true
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
@ -217,6 +229,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true 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 -> { R.id.menu_overlay_controls -> {
showOverlayOptions() showOverlayOptions()
true true
@ -332,8 +353,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
launch { launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.RESUMED) {
driverViewModel.isDriverReady.collect { driverViewModel.isInteractionAllowed.collect {
if (it && !emulationState.isRunning) { if (it) {
onEmulationStart()
}
}
}
}
}
}
private fun onEmulationStart() {
if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
if (!DirectoryInitialization.areDirectoriesReady) { if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start() DirectoryInitialization.start()
} }
@ -343,10 +374,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
emulationState.run(emulationActivity!!.isActivityRecreated) emulationState.run(emulationActivity!!.isActivityRecreated)
} }
} }
}
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
@ -435,7 +462,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
private fun updateOrientation() { private fun updateOrientation() {
emulationActivity?.let { emulationActivity?.let {
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) { it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) {
Settings.LayoutOption_MobileLandscape -> Settings.LayoutOption_MobileLandscape ->
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
Settings.LayoutOption_MobilePortrait -> Settings.LayoutOption_MobilePortrait ->
@ -617,7 +644,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
private fun startConfiguringControls() { private fun startConfiguringControls() {
// Lock the current orientation to prevent editing inconsistencies // 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 { emulationActivity?.let {
it.requestedOrientation = it.requestedOrientation =
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
@ -635,7 +662,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.doneControlConfig.visibility = View.GONE binding.doneControlConfig.visibility = View.GONE
binding.surfaceInputOverlay.setIsInEditMode(false) binding.surfaceInputOverlay.setIsInEditMode(false)
// Unlock the orientation if it was locked for editing // 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 { emulationActivity?.let {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} }

View File

@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
class GameFolderPropertiesDialogFragment : DialogFragment() { class GameFolderPropertiesDialogFragment : DialogFragment() {
@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
.show() .show()
} }
override fun onStop() {
super.onStop()
NativeConfig.saveGlobalConfig()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putBoolean(DEEP_SCAN, deepScan) outState.putBoolean(DEEP_SCAN, deepScan)

View File

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

View File

@ -0,0 +1,456 @@
// 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.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
}
// 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 = 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.apply {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.openImportSaves.collect {
if (it) {
importSaves.launch(arrayOf("application/zip"))
homeViewModel.setOpenImportSaves(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.reloadPropertiesList.collect {
if (it) {
reloadList()
homeViewModel.reloadPropertiesList(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,
R.drawable.ic_save,
{
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()
homeViewModel.reloadPropertiesList(true)
}
).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)
}
}
) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.clear_shader_cache,
descriptionId = R.string.clear_shader_cache_warning_description,
positiveAction = {
shaderCacheDir.deleteRecursively()
Toast.makeText(
YuzuApplication.appContext,
R.string.cleared_shaders_successfully,
Toast.LENGTH_SHORT
).show()
homeViewModel.reloadPropertiesList(true)
}
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
)
}
}
}
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()
homeViewModel.reloadPropertiesList(true)
}
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)
}
}

View File

@ -68,6 +68,9 @@ class HomeSettingsFragment : Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 mainActivity = requireActivity() as MainActivity
val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply { val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply {
@ -91,13 +94,14 @@ class HomeSettingsFragment : Fragment() {
R.string.install_gpu_driver_description, R.string.install_gpu_driver_description,
R.drawable.ic_build, R.drawable.ic_build,
{ {
binding.root.findNavController() val action = HomeSettingsFragmentDirections
.navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment) .actionHomeSettingsFragmentToDriverManagerFragment(null)
binding.root.findNavController().navigate(action)
}, },
{ GpuDriverHelper.supportsCustomDriverLoading() }, { GpuDriverHelper.supportsCustomDriverLoading() },
R.string.custom_driver_not_supported, R.string.custom_driver_not_supported,
R.string.custom_driver_not_supported_description, R.string.custom_driver_not_supported_description,
driverViewModel.selectedDriverMetadata driverViewModel.selectedDriverTitle
) )
) )
add( add(
@ -212,8 +216,11 @@ class HomeSettingsFragment : Fragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
exitTransition = null exitTransition = null
homeViewModel.setNavigationVisibility(visible = true, animated = true) }
homeViewModel.setStatusBarShadeVisibility(visible = true)
override fun onResume() {
super.onResume()
driverViewModel.updateDriverNameForGame(null)
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
activity: FragmentActivity, activity: FragmentActivity,
titleId: Int, titleId: Int,
cancellable: Boolean = false, cancellable: Boolean = false,
task: () -> Any task: suspend () -> Any
): IndeterminateProgressDialogFragment { ): IndeterminateProgressDialogFragment {
val dialog = IndeterminateProgressDialogFragment() val dialog = IndeterminateProgressDialogFragment()
val args = Bundle() val args = Bundle()

View File

@ -21,8 +21,6 @@ import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.Installable import org.yuzu.yuzu_emu.model.Installable
import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.ui.main.MainActivity
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class InstallableFragment : Fragment() { class InstallableFragment : Fragment() {
private var _binding: FragmentInstallablesBinding? = null private var _binding: FragmentInstallablesBinding? = null
@ -75,28 +73,6 @@ class InstallableFragment : Fragment() {
R.string.install_firmware_description, R.string.install_firmware_description,
install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } 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( Installable(
R.string.install_prod_keys, R.string.install_prod_keys,
R.string.install_prod_keys_description, R.string.install_prod_keys_description,

View File

@ -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 = 1
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, 1) { _: 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
}
}
}

View File

@ -27,30 +27,31 @@ class MessageDialogFragment : DialogFragment() {
val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!! val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
val helpLinkId = requireArguments().getInt(HELP_LINK) val helpLinkId = requireArguments().getInt(HELP_LINK)
val dialog = MaterialAlertDialogBuilder(requireContext()) val builder = MaterialAlertDialogBuilder(requireContext())
.setPositiveButton(R.string.close, null)
if (titleId != 0) dialog.setTitle(titleId) if (messageDialogViewModel.positiveAction == null) {
if (titleString.isNotEmpty()) dialog.setTitle(titleString) builder.setPositiveButton(R.string.close, null)
} else {
builder.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
messageDialogViewModel.positiveAction?.invoke()
}.setNegativeButton(android.R.string.cancel, null)
}
if (titleId != 0) builder.setTitle(titleId)
if (titleString.isNotEmpty()) builder.setTitle(titleString)
if (descriptionId != 0) { if (descriptionId != 0) {
dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY)) builder.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
} }
if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString) if (descriptionString.isNotEmpty()) builder.setMessage(descriptionString)
if (helpLinkId != 0) { if (helpLinkId != 0) {
dialog.setNeutralButton(R.string.learn_more) { _, _ -> builder.setNeutralButton(R.string.learn_more) { _, _ ->
openLink(getString(helpLinkId)) openLink(getString(helpLinkId))
} }
} }
return dialog.show() return builder.show()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
messageDialogViewModel.dismissAction.invoke()
messageDialogViewModel.clear()
} }
private fun openLink(link: String) { private fun openLink(link: String) {
@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() {
descriptionId: Int = 0, descriptionId: Int = 0,
descriptionString: String = "", descriptionString: String = "",
helpLinkId: Int = 0, helpLinkId: Int = 0,
dismissAction: () -> Unit = {} positiveAction: (() -> Unit)? = null
): MessageDialogFragment { ): MessageDialogFragment {
val dialog = MessageDialogFragment() val dialog = MessageDialogFragment()
val bundle = Bundle() val bundle = Bundle()
@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() {
putString(DESCRIPTION_STRING, descriptionString) putString(DESCRIPTION_STRING, descriptionString)
putInt(HELP_LINK, helpLinkId) putInt(HELP_LINK, helpLinkId)
} }
ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction = ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
dismissAction clear()
this.positiveAction = positiveAction
}
dialog.arguments = bundle dialog.arguments = bundle
return dialog return dialog
} }

View File

@ -24,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import info.debatty.java.stringsimilarity.Jaccard import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler import info.debatty.java.stringsimilarity.JaroWinkler
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import java.util.Locale
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
@ -60,7 +61,9 @@ class SearchFragment : Fragment() {
// This is using the correct scope, lint is just acting up // This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector") @SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
homeViewModel.setNavigationVisibility(visible = true, animated = false) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(true)
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -99,7 +102,7 @@ class SearchFragment : Fragment() {
} }
launch { launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.CREATED) {
gamesViewModel.games.collect { filterAndSearch() } gamesViewModel.games.collectLatest { filterAndSearch() }
} }
} }
launch { launch {

View File

@ -70,7 +70,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
sliderBinding = DialogSliderBinding.inflate(layoutInflater) sliderBinding = DialogSliderBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as SliderSetting val item = settingsViewModel.clickedItem as SliderSetting
settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units) settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
sliderBinding.slider.apply { sliderBinding.slider.apply {
valueFrom = item.min.toFloat() valueFrom = item.min.toFloat()
valueTo = item.max.toFloat() valueTo = item.max.toFloat()
@ -136,18 +136,18 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
is SingleChoiceSetting -> { is SingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which) val value = getValueForSingleChoiceSelection(scSetting, which)
scSetting.selectedValue = value scSetting.setSelectedValue(value)
} }
is StringSingleChoiceSetting -> { is StringSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which) val value = scSetting.getValueAt(which)
scSetting.selectedValue = value scSetting.setSelectedValue(value)
} }
is SliderSetting -> { is SliderSetting -> {
val sliderSetting = settingsViewModel.clickedItem as SliderSetting val sliderSetting = settingsViewModel.clickedItem as SliderSetting
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
} }
} }
closeDialog() closeDialog()
@ -171,7 +171,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
} }
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.selectedValue val value = item.getSelectedValue()
val valuesId = item.valuesId val valuesId = item.valuesId
if (valuesId > 0) { if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId) val valuesArray = requireContext().resources.getIntArray(valuesId)
@ -211,7 +211,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!") throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress( SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
(clickedItem as SliderSetting).selectedValue.toFloat() (clickedItem as SliderSetting).getSelectedValue().toFloat()
) )
} }
settingsViewModel.clickedItem = clickedItem settingsViewModel.clickedItem = clickedItem

View File

@ -304,6 +304,11 @@ class SetupFragment : Fragment() {
setInsets() setInsets()
} }
override fun onStop() {
super.onStop()
NativeConfig.saveGlobalConfig()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
if (_binding != null) { if (_binding != null) {

View File

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

View File

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

View File

@ -7,60 +7,55 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.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.FileUtil
import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
import org.yuzu.yuzu_emu.utils.NativeConfig
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
class DriverViewModel : ViewModel() { class DriverViewModel : ViewModel() {
private val _areDriversLoading = MutableStateFlow(false) private val _areDriversLoading = MutableStateFlow(false)
val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
private val _isDriverReady = MutableStateFlow(true) private val _isDriverReady = MutableStateFlow(true)
val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
private val _isDeletingDrivers = MutableStateFlow(false) 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 val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
var previouslySelectedDriver = 0 var previouslySelectedDriver = 0
var selectedDriver = -1 var selectedDriver = -1
private val _selectedDriverMetadata = // Used for showing which driver is currently installed within the driver manager card
MutableStateFlow( private val _selectedDriverTitle = MutableStateFlow("")
GpuDriverHelper.customDriverData.name val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
)
val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
private val _newDriverInstalled = MutableStateFlow(false) private val _newDriverInstalled = MutableStateFlow(false)
val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
val driversToDelete = mutableListOf<String>() val driversToDelete = mutableListOf<String>()
val isInteractionAllowed
get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
init { init {
_areDriversLoading.value = true val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData
viewModelScope.launch { findSelectedDriver(currentDriverMetadata)
withContext(Dispatchers.IO) {
val drivers = GpuDriverHelper.getDrivers()
val currentDriverMetadata = GpuDriverHelper.customDriverData
for (i in drivers.indices) {
if (drivers[i].second == currentDriverMetadata) {
setSelectedDriverIndex(i)
break
}
}
// If a user had installed a driver before the manager was implemented, this zips // 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 // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
@ -74,14 +69,21 @@ class DriverViewModel : ViewModel() {
GpuDriverHelper.driverInstallationPath!!, GpuDriverHelper.driverInstallationPath!!,
BufferedOutputStream(driverToSave.outputStream()) BufferedOutputStream(driverToSave.outputStream())
) )
drivers.add(Pair(driverToSave.path, currentDriverMetadata)) _driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
setSelectedDriverIndex(drivers.size - 1) setSelectedDriverIndex(_driverList.value.size - 1)
} }
_driverList.value = drivers // If a user had installed a driver before the config was reworked to be multiplatform,
_areDriversLoading.value = false // 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) { fun setSelectedDriverIndex(value: Int) {
@ -98,9 +100,9 @@ class DriverViewModel : ViewModel() {
fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
val driverIndex = _driverList.value.indexOfFirst { it == driverData } val driverIndex = _driverList.value.indexOfFirst { it == driverData }
if (driverIndex == -1) { if (driverIndex == -1) {
setSelectedDriverIndex(_driverList.value.size)
_driverList.value.add(driverData) _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) ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
} else { } else {
setSelectedDriverIndex(driverIndex) setSelectedDriverIndex(driverIndex)
@ -111,8 +113,31 @@ class DriverViewModel : ViewModel() {
_driverList.value.remove(driverData) _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 _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 { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
driversToDelete.forEach { driversToDelete.forEach {
@ -125,23 +150,29 @@ class DriverViewModel : ViewModel() {
_isDeletingDrivers.value = false _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 return
} }
_isDriverReady.value = false
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (selectedDriver == 0) { if (selectedDriverMetadata.name == null) {
GpuDriverHelper.installDefaultDriver() GpuDriverHelper.installDefaultDriver()
setDriverReady() setDriverReady()
return@withContext return@withContext
} }
val driverToInstall = File(driverList.value[selectedDriver].first) if (selectedDriverFile.exists()) {
if (driverToInstall.exists()) { GpuDriverHelper.installCustomDriver(selectedDriverFile)
GpuDriverHelper.installCustomDriver(driverToInstall)
} else { } else {
GpuDriverHelper.installDefaultDriver() 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() { private fun setDriverReady() {
_isDriverReady.value = true _isDriverReady.value = true
_selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
} }
} }

View File

@ -3,10 +3,18 @@
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.model
import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import java.util.HashSet import java.util.HashSet
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Parcelize @Parcelize
@Serializable @Serializable
@ -15,12 +23,44 @@ class Game(
val path: String, val path: String,
val programId: String = "", val programId: String = "",
val developer: String = "", val developer: String = "",
val version: String = "", var version: String = "",
val isHomebrew: Boolean = false val isHomebrew: Boolean = false
) : Parcelable { ) : Parcelable {
val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime" val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
val keyLastPlayedTime get() = "${path}_LastPlayed" val keyLastPlayedTime get() = "${path}_LastPlayed"
val settingsName: String
get() {
val programIdLong = programId.toLong()
return if (programIdLong == 0L) {
FileUtil.getFilename(Uri.parse(path))
} else {
"0" + programIdLong.toString(16).uppercase()
}
}
val programIdHex: String
get() {
val programIdLong = programId.toLong()
return if (programIdLong == 0L) {
"0"
} else {
"0" + programIdLong.toString(16).uppercase()
}
}
val saveZipName: String
get() = "$title ${YuzuApplication.appContext.getString(R.string.save_data).lowercase()} - ${
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
}.zip"
val saveDir: String
get() = DirectoryInitialization.userDirectory + "/nand" +
NativeLibrary.getSavePath(programId)
val addonDir: String
get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/"
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other !is Game) { if (other !is Game) {
return false return false
@ -34,6 +74,7 @@ class Game(
result = 31 * result + path.hashCode() result = 31 * result + path.hashCode()
result = 31 * result + programId.hashCode() result = 31 * result + programId.hashCode()
result = 31 * result + developer.hashCode() result = 31 * result + developer.hashCode()
result = 31 * result + version.hashCode()
result = 31 * result + isHomebrew.hashCode() result = 31 * result + isHomebrew.hashCode()
return result return result
} }

View File

@ -0,0 +1,36 @@
// 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:StringRes
val descriptionId: Int
@get:DrawableRes
val iconId: Int
}
data class SubmenuProperty(
override val titleId: Int,
override val descriptionId: Int,
override 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,
override val iconId: Int,
val install: (() -> Unit)? = null,
val export: (() -> Unit)? = null
) : GameProperty

View File

@ -20,8 +20,8 @@ import kotlinx.serialization.json.Json
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.utils.GameHelper import org.yuzu.yuzu_emu.utils.GameHelper
import org.yuzu.yuzu_emu.utils.GameMetadata
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
import java.util.concurrent.atomic.AtomicBoolean
class GamesViewModel : ViewModel() { class GamesViewModel : ViewModel() {
val games: StateFlow<List<Game>> get() = _games val games: StateFlow<List<Game>> get() = _games
@ -33,6 +33,8 @@ class GamesViewModel : ViewModel() {
val isReloading: StateFlow<Boolean> get() = _isReloading val isReloading: StateFlow<Boolean> get() = _isReloading
private val _isReloading = MutableStateFlow(false) private val _isReloading = MutableStateFlow(false)
private val reloading = AtomicBoolean(false)
val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData
private val _shouldSwapData = MutableStateFlow(false) private val _shouldSwapData = MutableStateFlow(false)
@ -49,38 +51,8 @@ class GamesViewModel : ViewModel() {
// Ensure keys are loaded so that ROM metadata can be decrypted. // Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys() NativeLibrary.reloadKeys()
// Retrieve list of cached games
val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.getStringSet(GameHelper.KEY_GAMES, emptySet())
viewModelScope.launch {
withContext(Dispatchers.IO) {
getGameDirs() getGameDirs()
if (storedGames!!.isNotEmpty()) { reloadGames(directoriesChanged = false, firstStartup = true)
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)
}
}
} }
fun setGames(games: List<Game>) { fun setGames(games: List<Game>) {
@ -110,16 +82,46 @@ class GamesViewModel : ViewModel() {
_searchFocused.value = searchFocused _searchFocused.value = searchFocused
} }
fun reloadGames(directoriesChanged: Boolean) { fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
if (isReloading.value) { if (reloading.get()) {
return return
} }
reloading.set(true)
_isReloading.value = true _isReloading.value = true
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { 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()) setGames(GameHelper.getGames())
reloading.set(false)
_isReloading.value = false _isReloading.value = false
if (directoriesChanged) { if (directoriesChanged) {
@ -168,6 +170,7 @@ class GamesViewModel : ViewModel() {
fun onCloseGameFoldersFragment() = fun onCloseGameFoldersFragment() =
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
NativeConfig.saveGlobalConfig()
getGameDirs(true) getGameDirs(true)
} }
} }

View File

@ -3,6 +3,7 @@
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.model
import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -21,6 +22,15 @@ class HomeViewModel : ViewModel() {
private val _gamesDirSelected = MutableStateFlow(false) private val _gamesDirSelected = MutableStateFlow(false)
val gamesDirSelected get() = _gamesDirSelected.asStateFlow() val gamesDirSelected get() = _gamesDirSelected.asStateFlow()
private val _openImportSaves = MutableStateFlow(false)
val openImportSaves get() = _openImportSaves.asStateFlow()
private val _contentToInstall = MutableStateFlow<List<Uri>?>(null)
val contentToInstall get() = _contentToInstall.asStateFlow()
private val _reloadPropertiesList = MutableStateFlow(false)
val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow()
var navigatedToSetup = false var navigatedToSetup = false
fun setNavigationVisibility(visible: Boolean, animated: Boolean) { fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
@ -44,4 +54,16 @@ class HomeViewModel : ViewModel() {
fun setGamesDirSelected(selected: Boolean) { fun setGamesDirSelected(selected: Boolean) {
_gamesDirSelected.value = selected _gamesDirSelected.value = selected
} }
fun setOpenImportSaves(import: Boolean) {
_openImportSaves.value = import
}
fun setContentToInstall(documents: List<Uri>?) {
_contentToInstall.value = documents
}
fun reloadPropertiesList(reload: Boolean) {
_reloadPropertiesList.value = reload
}
} }

View File

@ -6,9 +6,9 @@ package org.yuzu.yuzu_emu.model
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
class MessageDialogViewModel : ViewModel() { class MessageDialogViewModel : ViewModel() {
var dismissAction: () -> Unit = {} var positiveAction: (() -> Unit)? = null
fun clear() { fun clear() {
dismissAction = {} positiveAction = null
} }
} }

View File

@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() {
fun setAdapterItemChanged(value: Int) { fun setAdapterItemChanged(value: Int) {
_adapterItemChanged.value = value _adapterItemChanged.value = value
} }
fun clear() {
game = null
}
} }

View File

@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() {
val cancelled: StateFlow<Boolean> get() = _cancelled val cancelled: StateFlow<Boolean> get() = _cancelled
private val _cancelled = MutableStateFlow(false) private val _cancelled = MutableStateFlow(false)
lateinit var task: () -> Any lateinit var task: suspend () -> Any
fun clear() { fun clear() {
_result.value = Any() _result.value = Any()

View File

@ -19,7 +19,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.transition.MaterialFadeThrough import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.GameAdapter import org.yuzu.yuzu_emu.adapters.GameAdapter
@ -35,11 +35,6 @@ class GamesFragment : Fragment() {
private val gamesViewModel: GamesViewModel by activityViewModels() private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialFadeThrough()
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -52,7 +47,9 @@ class GamesFragment : Fragment() {
// This is using the correct scope, lint is just acting up // This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector") @SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
homeViewModel.setNavigationVisibility(visible = true, animated = false) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(true)
binding.gridGames.apply { binding.gridGames.apply {
layoutManager = AutofitGridLayoutManager( layoutManager = AutofitGridLayoutManager(
@ -99,7 +96,7 @@ class GamesFragment : Fragment() {
} }
launch { launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.RESUMED) {
gamesViewModel.games.collect { gamesViewModel.games.collectLatest {
(binding.gridGames.adapter as GameAdapter).submitList(it) (binding.gridGames.adapter as GameAdapter).submitList(it)
if (it.isEmpty()) { if (it.isEmpty()) {
binding.noticeText.visibility = View.VISIBLE binding.noticeText.visibility = View.VISIBLE

View File

@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope
import java.io.File import java.io.File
import java.io.FilenameFilter import java.io.FilenameFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
@ -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.AddGameFolderDialogFragment
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.getPublicFilesDir import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.model.TaskState
@ -60,15 +57,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
private val homeViewModel: HomeViewModel by viewModels() private val homeViewModel: HomeViewModel by viewModels()
private val gamesViewModel: GamesViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels()
private val taskViewModel: TaskViewModel by viewModels() private val taskViewModel: TaskViewModel by viewModels()
private val addonViewModel: AddonViewModel by viewModels()
override var themeId: Int = 0 override var themeId: Int = 0
private val savesFolder
get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
// Get first subfolder in saves folder (should be the user folder)
val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@ -145,6 +137,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
} }
} }
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.contentToInstall.collect {
if (it != null) {
installContent(it)
homeViewModel.setContentToInstall(null)
}
}
}
}
} }
// Dismiss previous notifications (should not happen unless a crash occurred) // Dismiss previous notifications (should not happen unless a crash occurred)
@ -253,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
super.onResume() super.onResume()
} }
override fun onStop() {
super.onStop()
CoroutineScope(Dispatchers.IO).launch {
NativeConfig.saveSettings()
}
}
override fun onDestroy() { override fun onDestroy() {
EmulationActivity.stopForegroundService(this) EmulationActivity.stopForegroundService(this)
super.onDestroy() super.onDestroy()
@ -468,7 +463,46 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val installGameUpdate = registerForActivityResult( val installGameUpdate = registerForActivityResult(
ActivityResultContracts.OpenMultipleDocuments() ActivityResultContracts.OpenMultipleDocuments()
) { documents: List<Uri> -> ) { documents: List<Uri> ->
if (documents.isNotEmpty()) { if (documents.isEmpty()) {
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( IndeterminateProgressDialogFragment.newInstance(
this@MainActivity, this@MainActivity,
R.string.installing_game_content R.string.installing_game_content
@ -507,6 +541,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
} }
addonViewModel.refreshAddons()
val separator = System.getProperty("line.separator") ?: "\n" val separator = System.getProperty("line.separator") ?: "\n"
val installResult = StringBuilder() val installResult = StringBuilder()
if (installSuccess > 0) { if (installSuccess > 0) {
@ -572,7 +608,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
}
val exportUserData = registerForActivityResult( val exportUserData = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip") ActivityResultContracts.CreateDocument("application/zip")
@ -632,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
// Clear existing user data // Clear existing user data
NativeConfig.unloadConfig() NativeConfig.unloadGlobalConfig()
File(DirectoryInitialization.userDirectory!!).deleteRecursively() File(DirectoryInitialization.userDirectory!!).deleteRecursively()
// Copy archive to internal storage // Copy archive to internal storage
@ -651,108 +686,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
// Reinitialize relevant data // Reinitialize relevant data
NativeLibrary.initializeSystem(true) NativeLibrary.initializeSystem(true)
NativeConfig.initializeConfig() NativeConfig.initializeGlobalConfig()
gamesViewModel.reloadGames(false) gamesViewModel.reloadGames(false)
return@newInstance getString(R.string.user_data_import_success) return@newInstance getString(R.string.user_data_import_success)
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
/**
* Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
*/
val exportSaves = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip")
) { result ->
if (result == null) {
return@registerForActivityResult
}
IndeterminateProgressDialogFragment.newInstance(
this,
R.string.save_files_exporting,
false
) {
val zipResult = FileUtil.zipFromInternalStorage(
File(savesFolderRoot),
savesFolderRoot,
BufferedOutputStream(contentResolver.openOutputStream(result))
)
return@newInstance when (zipResult) {
TaskState.Completed -> getString(R.string.export_success)
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
}
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
private val startForResultExportSave =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
}
val importSaves =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {
return@registerForActivityResult
}
NativeLibrary.initializeEmptyUserDirectory()
val inputZip = contentResolver.openInputStream(result)
// A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
var validZip = false
val savesFolder = File(savesFolderRoot)
val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/")
cacheSaveDir.mkdir()
if (inputZip == null) {
Toast.makeText(
applicationContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
return@registerForActivityResult
}
val filterTitleId =
FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
try {
CoroutineScope(Dispatchers.IO).launch {
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
File(savesFolder, savePath).deleteRecursively()
File(cacheSaveDir, savePath).copyRecursively(
File(savesFolder, savePath),
true
)
validZip = true
}
withContext(Dispatchers.Main) {
if (!validZip) {
MessageDialogFragment.newInstance(
this@MainActivity,
titleId = R.string.save_file_invalid_zip_structure,
descriptionId = R.string.save_file_invalid_zip_structure_description
).show(supportFragmentManager, MessageDialogFragment.TAG)
return@withContext
}
Toast.makeText(
applicationContext,
getString(R.string.save_file_imported_success),
Toast.LENGTH_LONG
).show()
}
cacheSaveDir.deleteRecursively()
}
} catch (e: Exception) {
Toast.makeText(
applicationContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
}
}
} }

View File

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

View File

@ -16,7 +16,7 @@ object DirectoryInitialization {
if (!areDirectoriesReady) { if (!areDirectoriesReady) {
initializeInternalStorage() initializeInternalStorage()
NativeLibrary.initializeSystem(false) NativeLibrary.initializeSystem(false)
NativeConfig.initializeConfig() NativeConfig.initializeGlobalConfig()
areDirectoriesReady = true areDirectoriesReady = true
} }
} }

View File

@ -22,6 +22,7 @@ import java.io.BufferedOutputStream
import java.lang.NullPointerException import java.lang.NullPointerException
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlin.IllegalStateException
object FileUtil { object FileUtil {
const val PATH_TREE = "tree" const val PATH_TREE = "tree"
@ -342,6 +343,37 @@ object FileUtil {
return TaskState.Completed return TaskState.Completed
} }
/**
* Helper function that copies the contents of a DocumentFile folder into a [File]
* @param file [File] representation of the folder to copy into
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
*/
fun DocumentFile.copyFilesTo(file: File) {
file.mkdirs()
if (!this.isDirectory || !file.isDirectory) {
throw IllegalStateException(
"[FileUtil] Tried to copy a folder into a file or vice versa"
)
}
this.listFiles().forEach {
val newFile = File(file, it.name!!)
if (it.isDirectory) {
newFile.mkdirs()
DocumentFile.fromTreeUri(YuzuApplication.appContext, it.uri)?.copyFilesTo(newFile)
} else {
val inputStream =
YuzuApplication.appContext.contentResolver.openInputStream(it.uri)
BufferedInputStream(inputStream).use { bos ->
if (!newFile.exists()) {
newFile.createNewFile()
}
newFile.outputStream().use { os -> bos.copyTo(os) }
}
}
}
}
fun isRootTreeUri(uri: Uri): Boolean { fun isRootTreeUri(uri: Uri): Boolean {
val paths = uri.pathSegments val paths = uri.pathSegments
return paths.size == 2 && PATH_TREE == paths[0] return paths.size == 2 && PATH_TREE == paths[0]

View File

@ -36,6 +36,12 @@ object GameHelper {
// Ensure keys are loaded so that ROM metadata can be decrypted. // Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys() 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>() val badDirs = mutableListOf<Int>()
gameDirs.forEachIndexed { index: Int, gameDir: GameDir -> gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
val gameDirUri = Uri.parse(gameDir.uriString) val gameDirUri = Uri.parse(gameDir.uriString)
@ -92,14 +98,24 @@ object GameHelper {
) )
} else { } else {
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) { 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() val filePath = uri.toString()
if (!GameMetadata.getIsValid(filePath)) {
return null
}
// Needed to update installed content information
NativeLibrary.addFileToFilesystemProvider(filePath)
var name = GameMetadata.getTitle(filePath) var name = GameMetadata.getTitle(filePath)
// If the game's title field is empty, use the filename. // If the game's title field is empty, use the filename.
@ -118,7 +134,7 @@ object GameHelper {
filePath, filePath,
programId, programId,
GameMetadata.getDeveloper(filePath), GameMetadata.getDeveloper(filePath),
GameMetadata.getVersion(filePath), GameMetadata.getVersion(filePath, false),
GameMetadata.getIsHomebrew(filePath) GameMetadata.getIsHomebrew(filePath)
) )

View File

@ -4,13 +4,15 @@
package org.yuzu.yuzu_emu.utils package org.yuzu.yuzu_emu.utils
object GameMetadata { object GameMetadata {
external fun getIsValid(path: String): Boolean
external fun getTitle(path: String): String external fun getTitle(path: String): String
external fun getProgramId(path: String): String external fun getProgramId(path: String): String
external fun getDeveloper(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 external fun getIcon(path: String): ByteArray

View File

@ -10,6 +10,8 @@ import java.io.File
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication 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.ZipException
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -44,7 +46,7 @@ object GpuDriverHelper {
NativeLibrary.initializeGpuDriver( NativeLibrary.initializeGpuDriver(
hookLibPath, hookLibPath,
driverInstallationPath, driverInstallationPath,
customDriverData.libraryName, installedCustomDriverData.libraryName,
fileRedirectionPath fileRedirectionPath
) )
} }
@ -190,6 +192,7 @@ object GpuDriverHelper {
} }
} }
} catch (_: ZipException) { } catch (_: ZipException) {
} catch (_: FileNotFoundException) {
} }
return GpuDriverMetadata() return GpuDriverMetadata()
} }
@ -197,9 +200,12 @@ object GpuDriverHelper {
external fun supportsCustomDriverLoading(): Boolean external fun supportsCustomDriverLoading(): Boolean
// Parse the custom driver metadata to retrieve the name. // Parse the custom driver metadata to retrieve the name.
val customDriverData: GpuDriverMetadata val installedCustomDriverData: GpuDriverMetadata
get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
val customDriverSettingData: GpuDriverMetadata
get() = getMetadataFromZip(File(StringSetting.DRIVER_PATH.getString()))
fun initializeDirectories() { fun initializeDirectories() {
// Ensure the file redirection directory exists. // Ensure the file redirection directory exists.
val fileRedirectionDir = File(fileRedirectionPath!!) val fileRedirectionDir = File(fileRedirectionPath!!)

View File

@ -27,13 +27,13 @@ object MemoryUtil {
const val Pb = Tb * 1024 const val Pb = Tb * 1024
const val Eb = Pb * 1024 const val Eb = Pb * 1024
private fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String = fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
when { when {
size < Kb -> { size < Kb -> {
context.getString( context.getString(
R.string.memory_formatted, R.string.memory_formatted,
size.hundredths, size.hundredths,
context.getString(R.string.memory_byte) context.getString(R.string.memory_byte_shorthand)
) )
} }
size < Mb -> { size < Mb -> {

View File

@ -7,56 +7,113 @@ import org.yuzu.yuzu_emu.model.GameDir
object NativeConfig { object NativeConfig {
/** /**
* Creates a Config object and opens the emulation config. * Loads global config.
*/ */
@Synchronized @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 @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 @Synchronized
external fun reloadSettings() external fun reloadGlobalConfig()
/** /**
* Saves settings values in memory to disk. * Saves global settings values in memory to disk.
*/ */
@Synchronized @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 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 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 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 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 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 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 setString(key: String, value: String)
external fun getIsRuntimeModifiable(key: String): Boolean external fun getIsRuntimeModifiable(key: String): Boolean
external fun getConfigHeader(category: Int): String
external fun getPairedSettingKey(key: String): 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 getIsSaveable(key: String): Boolean
external fun getDefaultToString(key: String): String
/** /**
* Gets every [GameDir] in AndroidSettings::values.game_dirs * Gets every [GameDir] in AndroidSettings::values.game_dirs
*/ */
@ -74,4 +131,23 @@ object NativeConfig {
*/ */
@Synchronized @Synchronized
external fun addGameDir(dir: GameDir) external fun addGameDir(dir: GameDir)
/**
* Gets an array of the addons that are disabled for a given game
*
* @param programId String representation of a game's program ID
* @return An array of disabled addons
*/
@Synchronized
external fun getDisabledAddons(programId: String): Array<String>
/**
* Clears the disabled addons array corresponding to [programId] and replaces them
* with [disabledAddons]
*
* @param programId String representation of a game's program ID
* @param disabledAddons Replacement array of disabled addons
*/
@Synchronized
external fun setDisabledAddons(programId: String, disabledAddons: Array<String>)
} }

View File

@ -36,6 +36,7 @@ void AndroidConfig::ReadAndroidValues() {
ReadAndroidUIValues(); ReadAndroidUIValues();
ReadUIValues(); ReadUIValues();
} }
ReadDriverValues();
} }
void AndroidConfig::ReadAndroidUIValues() { void AndroidConfig::ReadAndroidUIValues() {
@ -57,6 +58,7 @@ void AndroidConfig::ReadUIValues() {
void AndroidConfig::ReadPathValues() { void AndroidConfig::ReadPathValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
AndroidSettings::values.game_dirs.clear();
const int gamedirs_size = BeginArray(std::string("gamedirs")); const int gamedirs_size = BeginArray(std::string("gamedirs"));
for (int i = 0; i < gamedirs_size; ++i) { for (int i = 0; i < gamedirs_size; ++i) {
SetArrayIndex(i); SetArrayIndex(i);
@ -71,11 +73,20 @@ void AndroidConfig::ReadPathValues() {
EndGroup(); EndGroup();
} }
void AndroidConfig::ReadDriverValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
ReadCategory(Settings::Category::GpuDriver);
EndGroup();
}
void AndroidConfig::SaveAndroidValues() { void AndroidConfig::SaveAndroidValues() {
if (global) { if (global) {
SaveAndroidUIValues(); SaveAndroidUIValues();
SaveUIValues(); SaveUIValues();
} }
SaveDriverValues();
WriteToIni(); WriteToIni();
} }
@ -111,6 +122,14 @@ void AndroidConfig::SavePathValues() {
EndGroup(); EndGroup();
} }
void AndroidConfig::SaveDriverValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
WriteCategory(Settings::Category::GpuDriver);
EndGroup();
}
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
auto& map = Settings::values.linkage.by_category; auto& map = Settings::values.linkage.by_category;
if (map.contains(category)) { if (map.contains(category)) {

View File

@ -17,6 +17,7 @@ public:
protected: protected:
void ReadAndroidValues(); void ReadAndroidValues();
void ReadAndroidUIValues(); void ReadAndroidUIValues();
void ReadDriverValues();
void ReadHidbusValues() override {} void ReadHidbusValues() override {}
void ReadDebugControlValues() override {} void ReadDebugControlValues() override {}
void ReadPathValues() override; void ReadPathValues() override;
@ -28,6 +29,7 @@ protected:
void SaveAndroidValues(); void SaveAndroidValues();
void SaveAndroidUIValues(); void SaveAndroidUIValues();
void SaveDriverValues();
void SaveHidbusValues() override {} void SaveHidbusValues() override {}
void SaveDebugControlValues() override {} void SaveDebugControlValues() override {}
void SavePathValues() override; void SavePathValues() override;

View File

@ -30,6 +30,9 @@ struct Values {
Settings::Specialization::Default, Settings::Specialization::Default,
true, true,
true}; true};
Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path",
Settings::Category::GpuDriver};
}; };
extern Values values; extern Values values;

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <core/core.h> #include <core/core.h>
#include <core/file_sys/mode.h>
#include <core/file_sys/patch_manager.h> #include <core/file_sys/patch_manager.h>
#include <core/loader/nro.h> #include <core/loader/nro.h>
#include <jni.h> #include <jni.h>
@ -61,7 +62,11 @@ RomMetadata CacheRomMetadata(const std::string& path) {
return entry; 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()) { if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
return search->second; return search->second;
} }
@ -71,6 +76,32 @@ RomMetadata GetRomMetadata(const std::string& path) {
extern "C" { 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 file_type = loader->GetFileType();
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
return false;
}
u64 program_id = 0;
Loader::ResultStatus res = loader->ReadProgramId(program_id);
if (res != Loader::ResultStatus::Success) {
return false;
}
return true;
}
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj, jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj,
jstring jpath) { jstring jpath) {
return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title); 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 Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj,
jstring jpath) { jstring jpath, jboolean jreload) {
return ToJString(env, GetRomMetadata(GetJString(env, jpath)).version); return ToJString(env, GetRomMetadata(GetJString(env, jpath), jreload).version);
} }
jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj, 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) { 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" } // extern "C"

View File

@ -20,6 +20,21 @@ static jmethodID s_disk_cache_load_progress;
static jmethodID s_on_emulation_started; static jmethodID s_on_emulation_started;
static jmethodID s_on_emulation_stopped; static jmethodID s_on_emulation_stopped;
static jclass s_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; static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
namespace IDCache { namespace IDCache {
@ -79,6 +94,58 @@ jmethodID GetOnEmulationStopped() {
return s_on_emulation_stopped; 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 } // namespace IDCache
#ifdef __cplusplus #ifdef __cplusplus
@ -115,6 +182,31 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
s_on_emulation_stopped = s_on_emulation_stopped =
env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V"); env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
const jclass 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 // Initialize Android Storage
Common::FS::Android::RegisterCallbacks(env, s_native_library_class); 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_disk_cache_progress_class);
env->DeleteGlobalRef(s_load_callback_stage_class); env->DeleteGlobalRef(s_load_callback_stage_class);
env->DeleteGlobalRef(s_game_dir_class); env->DeleteGlobalRef(s_game_dir_class);
env->DeleteGlobalRef(s_game_class);
env->DeleteGlobalRef(s_string_class);
env->DeleteGlobalRef(s_pair_class);
// UnInitialize applets // UnInitialize applets
SoftwareKeyboard::CleanupJNI(env); SoftwareKeyboard::CleanupJNI(env);

View File

@ -20,4 +20,19 @@ jmethodID GetDiskCacheLoadProgress();
jmethodID GetOnEmulationStarted(); jmethodID GetOnEmulationStarted();
jmethodID GetOnEmulationStopped(); 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 } // namespace IDCache

View File

@ -14,6 +14,7 @@
#include <android/api-level.h> #include <android/api-level.h>
#include <android/native_window_jni.h> #include <android/native_window_jni.h>
#include <common/fs/fs.h> #include <common/fs/fs.h>
#include <core/file_sys/patch_manager.h>
#include <core/file_sys/savedata_factory.h> #include <core/file_sys/savedata_factory.h>
#include <core/loader/nro.h> #include <core/loader/nro.h>
#include <jni.h> #include <jni.h>
@ -79,6 +80,10 @@ Core::System& EmulationSession::System() {
return m_system; return m_system;
} }
FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
return m_manual_provider.get();
}
const EmuWindow_Android& EmulationSession::Window() const { const EmuWindow_Android& EmulationSession::Window() const {
return *m_window; return *m_window;
} }
@ -455,6 +460,15 @@ void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
static_cast<jint>(result)); static_cast<jint>(result));
} }
u64 EmulationSession::GetProgramId(JNIEnv* env, jstring jprogramId) {
auto program_id_string = GetJString(env, jprogramId);
try {
return std::stoull(program_id_string);
} catch (...) {
return 0;
}
}
static Core::SystemResultStatus RunEmulation(const std::string& filepath) { static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
MicroProfileOnThreadCreate("EmuThread"); MicroProfileOnThreadCreate("EmuThread");
SCOPE_EXIT({ MicroProfileShutdown(); }); SCOPE_EXIT({ MicroProfileShutdown(); });
@ -504,6 +518,27 @@ int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject
GetJString(env, j_file_extension)); GetJString(env, j_file_extension));
} }
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
jstring jprogramId,
jstring jupdatePath) {
u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
std::string updatePath = GetJString(env, jupdatePath);
std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>(
EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(updatePath,
FileSys::Mode::Read));
for (const auto& item : nsp->GetNCAs()) {
for (const auto& nca_details : item.second) {
if (nca_details.second->GetName().ends_with(".cnmt.nca")) {
auto update_id = nca_details.second->GetTitleId() & ~0xFFFULL;
if (update_id == program_id) {
return true;
}
}
}
}
return false;
}
void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz,
jstring hook_lib_dir, jstring hook_lib_dir,
jstring custom_driver_dir, jstring custom_driver_dir,
@ -665,13 +700,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass
EmulationSession::GetInstance().InitializeSystem(reload); EmulationSession::GetInstance().InitializeSystem(reload);
} }
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass clazz) {
return {};
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z(
JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate) {}
jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) { jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) {
jdoubleArray j_stats = env->NewDoubleArray(4); jdoubleArray j_stats = env->NewDoubleArray(4);
@ -696,9 +724,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass
return ToJString(env, "JIT"); return ToJString(env, "JIT");
} }
void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env, void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
jclass clazz, EmulationSession::GetInstance().System().ApplySettings();
jstring j_path) {} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {
Settings::LogSettings();
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz, void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz,
jstring j_path) { jstring j_path) {
@ -792,4 +824,69 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
return true; return true;
} }
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj,
jstring jpath,
jstring jprogramId) {
const auto path = GetJString(env, jpath);
const auto vFile =
Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
if (vFile == nullptr) {
return nullptr;
}
auto& system = EmulationSession::GetInstance().System();
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
const FileSys::PatchManager pm{program_id, system.GetFileSystemController(),
system.GetContentProvider()};
const auto loader = Loader::GetLoader(system, vFile);
FileSys::VirtualFile update_raw;
loader->ReadUpdateRaw(update_raw);
auto addons = pm.GetPatchVersionNames(update_raw);
auto jemptyString = ToJString(env, "");
auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
jemptyString, jemptyString);
jobjectArray jaddonsArray =
env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
int i = 0;
for (const auto& addon : addons) {
jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
ToJString(env, addon.first), ToJString(env, addon.second));
env->SetObjectArrayElement(jaddonsArray, i, jaddon);
++i;
}
return jaddonsArray;
}
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
jstring jprogramId) {
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
auto& system = EmulationSession::GetInstance().System();
Service::Account::ProfileManager manager;
// TODO: Pass in a selected user once we get the relevant UI working
const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
ASSERT(user_id);
const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir),
FileSys::Mode::Read);
const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
program_id, user_id->AsU128(), 0);
return ToJString(env, user_save_data_path);
}
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().GetContentProvider()->ClearAllEntries();
}
} // extern "C" } // extern "C"

View File

@ -21,6 +21,7 @@ public:
static EmulationSession& GetInstance(); static EmulationSession& GetInstance();
const Core::System& System() const; const Core::System& System() const;
Core::System& System(); Core::System& System();
FileSys::ManualContentProvider* GetContentProvider();
const EmuWindow_Android& Window() const; const EmuWindow_Android& Window() const;
EmuWindow_Android& Window(); EmuWindow_Android& Window();
@ -54,6 +55,8 @@ public:
static void OnEmulationStarted(); static void OnEmulationStarted();
static u64 GetProgramId(JNIEnv* env, jstring jprogramId);
private: private:
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max); static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
static void OnEmulationStopped(Core::SystemResultStatus result); static void OnEmulationStopped(Core::SystemResultStatus result);

View File

@ -3,6 +3,7 @@
#include <string> #include <string>
#include <common/fs/fs_util.h>
#include <jni.h> #include <jni.h>
#include "android_config.h" #include "android_config.h"
@ -12,19 +13,21 @@
#include "frontend_common/config.h" #include "frontend_common/config.h"
#include "jni/android_common/android_common.h" #include "jni/android_common/android_common.h"
#include "jni/id_cache.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> template <typename T>
Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
auto key = GetJString(env, jkey); auto key = GetJString(env, jkey);
auto basicSetting = Settings::values.linkage.by_key[key]; auto basic_setting = Settings::values.linkage.by_key[key];
auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key]; if (basic_setting != 0) {
if (basicSetting != 0) { return static_cast<Settings::Setting<T>*>(basic_setting);
return static_cast<Settings::Setting<T>*>(basicSetting);
} }
if (basicAndroidSetting != 0) { auto basic_android_setting = AndroidSettings::values.linkage.by_key[key];
return static_cast<Settings::Setting<T>*>(basicAndroidSetting); if (basic_android_setting != 0) {
return static_cast<Settings::Setting<T>*>(basic_android_setting);
} }
LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key); LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
return nullptr; return nullptr;
@ -32,35 +35,52 @@ Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
extern "C" { extern "C" {
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeConfig(JNIEnv* env, jobject obj) { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
config = std::make_unique<AndroidConfig>(); global_config = std::make_unique<AndroidConfig>();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadConfig(JNIEnv* env, jobject obj) { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
config.reset(); global_config.reset();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadSettings(JNIEnv* env, jobject obj) { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) {
config->AndroidConfig::ReloadAllValues(); global_config->AndroidConfig::ReloadAllValues();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobject obj) { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) {
config->AndroidConfig::SaveAllValues(); 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, 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); auto setting = getSetting<bool>(env, jkey);
if (setting == nullptr) { if (setting == nullptr) {
return false; return false;
} }
setting->SetGlobal(true); return setting->GetValue(static_cast<bool>(needGlobal));
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey, 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) { if (setting == nullptr) {
return; return;
} }
setting->SetGlobal(true);
setting->SetValue(static_cast<bool>(value)); setting->SetValue(static_cast<bool>(value));
} }
jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey, 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); auto setting = getSetting<u8>(env, jkey);
if (setting == nullptr) { if (setting == nullptr) {
return -1; return -1;
} }
setting->SetGlobal(true); return setting->GetValue(static_cast<bool>(needGlobal));
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey, 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) { if (setting == nullptr) {
return; return;
} }
setting->SetGlobal(true);
setting->SetValue(value); setting->SetValue(value);
} }
jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey, 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); auto setting = getSetting<u16>(env, jkey);
if (setting == nullptr) { if (setting == nullptr) {
return -1; return -1;
} }
setting->SetGlobal(true); return setting->GetValue(static_cast<bool>(needGlobal));
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey, 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) { if (setting == nullptr) {
return; return;
} }
setting->SetGlobal(true);
setting->SetValue(value); setting->SetValue(value);
} }
jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey, 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); auto setting = getSetting<int>(env, jkey);
if (setting == nullptr) { if (setting == nullptr) {
return -1; return -1;
} }
setting->SetGlobal(true); return setting->GetValue(needGlobal);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey, 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) { if (setting == nullptr) {
return; return;
} }
setting->SetGlobal(true);
setting->SetValue(value); setting->SetValue(value);
} }
jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey, 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); auto setting = getSetting<float>(env, jkey);
if (setting == nullptr) { if (setting == nullptr) {
return -1; return -1;
} }
setting->SetGlobal(true); return setting->GetValue(static_cast<bool>(needGlobal));
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey, 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) { if (setting == nullptr) {
return; return;
} }
setting->SetGlobal(true);
setting->SetValue(value); setting->SetValue(value);
} }
jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey, jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) { jboolean needGlobal) {
auto setting = getSetting<long>(env, jkey); auto setting = getSetting<s64>(env, jkey);
if (setting == nullptr) { if (setting == nullptr) {
return -1; return -1;
} }
setting->SetGlobal(true); return setting->GetValue(static_cast<bool>(needGlobal));
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey, 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) { if (setting == nullptr) {
return; return;
} }
setting->SetGlobal(true);
setting->SetValue(value); setting->SetValue(value);
} }
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey, 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); auto setting = getSetting<std::string>(env, jkey);
if (setting == nullptr) { if (setting == nullptr) {
return ToJString(env, ""); return ToJString(env, "");
} }
setting->SetGlobal(true); return ToJString(env, setting->GetValue(static_cast<bool>(needGlobal)));
if (static_cast<bool>(getDefault)) {
return ToJString(env, setting->GetDefault());
}
return ToJString(env, setting->GetValue());
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey, 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; return;
} }
setting->SetGlobal(true);
setting->SetValue(GetJString(env, value)); setting->SetValue(GetJString(env, value));
} }
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj, jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj,
jstring jkey) { jstring jkey) {
auto key = GetJString(env, jkey); auto setting = getSetting<std::string>(env, jkey);
auto setting = Settings::values.linkage.by_key[key]; if (setting != nullptr) {
if (setting != 0) {
return setting->RuntimeModfiable(); return setting->RuntimeModfiable();
} }
LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
return true; 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 Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj,
jstring jkey) { jstring jkey) {
auto setting = getSetting<std::string>(env, jkey); auto setting = getSetting<std::string>(env, jkey);
@ -254,6 +223,50 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e
return ToJString(env, setting->PairedSetting()->GetLabel()); 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));
}
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSaveable(JNIEnv* env, jobject obj,
jstring jkey) {
auto setting = getSetting<std::string>(env, jkey);
if (setting != nullptr) {
return setting->Save();
}
return false;
}
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) { jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) {
jclass gameDirClass = IDCache::GetGameDirClass(); jclass gameDirClass = IDCache::GetGameDirClass();
jmethodID gameDirConstructor = IDCache::GetGameDirConstructor(); jmethodID gameDirConstructor = IDCache::GetGameDirConstructor();
@ -305,4 +318,30 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
} }
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDisabledAddons(JNIEnv* env, jobject obj,
jstring jprogramId) {
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
auto& disabledAddons = Settings::values.disabled_addons[program_id];
jobjectArray jdisabledAddonsArray =
env->NewObjectArray(disabledAddons.size(), IDCache::GetStringClass(), ToJString(env, ""));
for (size_t i = 0; i < disabledAddons.size(); ++i) {
env->SetObjectArrayElement(jdisabledAddonsArray, i, ToJString(env, disabledAddons[i]));
}
return jdisabledAddonsArray;
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setDisabledAddons(JNIEnv* env, jobject obj,
jstring jprogramId,
jobjectArray jdisabledAddons) {
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
Settings::values.disabled_addons[program_id].clear();
std::vector<std::string> disabled_addons;
const int size = env->GetArrayLength(jdisabledAddons);
for (int i = 0; i < size; ++i) {
auto jaddon = static_cast<jstring>(env->GetObjectArrayElement(jdisabledAddons, i));
disabled_addons.push_back(GetJString(env, jaddon));
}
Settings::values.disabled_addons[program_id] = disabled_addons;
}
} // extern "C" } // extern "C"

View File

@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="960" android:viewportWidth="24"
android:viewportHeight="960" android:viewportHeight="24">
android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="?attr/colorControlNormal"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L647,120Q663,120 677.5,126Q692,132 703,143L817,257Q828,268 834,282.5Q840,297 840,313L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM280,400L560,400Q577,400 588.5,388.5Q600,377 600,360L600,280Q600,263 588.5,251.5Q577,240 560,240L280,240Q263,240 251.5,251.5Q240,263 240,280L240,360Q240,377 251.5,388.5Q263,400 280,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/> android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector> </vector>

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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"
style="?attr/materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="20dp"
android:layout_gravity="center_vertical"
app:tint="?attr/colorOnSurface"
tools:src="@drawable/ic_settings" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:orientation="vertical">
<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:text="@string/user_data"
android:textAlignment="viewStart" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/description"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/user_data_description"
android:textAlignment="viewStart" />
</LinearLayout>
<Button
android:id="@+id/button_export"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:contentDescription="@string/export"
android:tooltipText="@string/export"
android:visibility="gone"
app:icon="@drawable/ic_export"
tools:visibility="visible" />
<Button
android:id="@+id/button_install"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:contentDescription="@string/string_import"
android:tooltipText="@string/string_import"
android:visibility="gone"
app:icon="@drawable/ic_import"
tools:visibility="visible" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

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

View File

@ -11,7 +11,8 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:paddingVertical="16dp"
android:paddingHorizontal="24dp"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_gravity="center"> android:layout_gravity="center">

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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"
style="?attr/materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="20dp"
android:layout_gravity="center_vertical"
app:tint="?attr/colorOnSurface"
tools:src="@drawable/ic_settings" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:orientation="vertical">
<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:text="@string/user_data"
android:textAlignment="viewStart" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/description"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/user_data_description"
android:textAlignment="viewStart" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/button_install"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:contentDescription="@string/string_import"
android:tooltipText="@string/string_import"
android:visibility="gone"
app:icon="@drawable/ic_import"
tools:visibility="visible" />
<Button
android:id="@+id/button_export"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="8dp"
android:contentDescription="@string/export"
android:tooltipText="@string/export"
android:visibility="gone"
app:icon="@drawable/ic_export"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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"
style="?attr/materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center"
android:paddingVertical="16dp"
android:paddingHorizontal="24dp">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="20dp"
android:layout_gravity="center_vertical"
app:tint="?attr/colorOnSurface" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_gravity="center_vertical">
<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:textAlignment="viewStart"
tools:text="@string/applets" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/description"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
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>
</com.google.android.material.card.MaterialCardView>

View 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>

View 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>

View File

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

View 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>

View File

@ -62,6 +62,16 @@
android:textSize="13sp" android:textSize="13sp"
tools:text="1x" /> 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>
</LinearLayout> </LinearLayout>

View File

@ -10,22 +10,23 @@
android:minHeight="72dp" android:minHeight="72dp"
android:padding="16dp"> android:padding="16dp">
<com.google.android.material.materialswitch.MaterialSwitch <LinearLayout
android:id="@+id/switch_widget" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:orientation="vertical">
android:layout_centerVertical="true" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true" android:orientation="horizontal">
android:layout_centerVertical="true"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:layout_toStartOf="@+id/switch_widget"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical"> android:orientation="vertical"
android:layout_weight="1">
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_name" android:id="@+id/text_setting_name"
@ -48,4 +49,24 @@
</LinearLayout> </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> </RelativeLayout>

View File

@ -11,6 +11,11 @@
android:icon="@drawable/ic_settings" android:icon="@drawable/ic_settings"
android:title="@string/preferences_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 <item
android:id="@+id/menu_overlay_controls" android:id="@+id/menu_overlay_controls"
android:icon="@drawable/ic_controller" android:icon="@drawable/ic_controller"

Some files were not shown because too many files have changed in this diff Show More