@ -1,11 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.os.Bundle
import android.provider.DocumentsContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -14,6 +19,7 @@ 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.lifecycleScope
@ -29,12 +35,14 @@ 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.DocumentProvider
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.SubMenuPropertySecondaryAction
import org.yuzu.yuzu_emu.model.SubmenuProperty
import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
@ -137,26 +145,67 @@ class GamePropertiesFragment : Fragment() {
SubmenuProperty (
R . string . info ,
R . string . info_description ,
R . drawable . ic_info_outline
) {
R . drawable . ic_info_outline ,
action = {
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
) {
R . drawable . ic_settings ,
action = {
val action = HomeNavigationDirections . actionGlobalSettingsActivity (
args . game ,
Settings . MenuTag . SECTION_ROOT
)
binding . root . findNavController ( ) . navigate ( action )
} ,
secondaryActions = buildList {
val configExists = File (
DirectoryInitialization . userDirectory +
" /config/custom/ " + args . game . settingsName + " .ini "
) . exists ( )
add ( SubMenuPropertySecondaryAction (
isShown = configExists ,
descriptionId = R . string . import_config ,
iconId = R . drawable . ic_import ,
action = {
importConfig . launch ( arrayOf ( " text/ini " , " application/octet-stream " ) )
}
) )
add ( SubMenuPropertySecondaryAction (
isShown = configExists ,
descriptionId = R . string . export_config ,
iconId = R . drawable . ic_export ,
action = {
exportConfig . launch ( args . game . settingsName + " .ini " )
}
) )
add ( SubMenuPropertySecondaryAction (
isShown = configExists ,
descriptionId = R . string . share_game_settings ,
iconId = R . drawable . ic_share ,
action = {
val configFile = File (
DirectoryInitialization . userDirectory +
" /config/custom/ " + args . game . settingsName + " .ini "
)
if ( configFile . exists ( ) ) {
shareConfigFile ( configFile )
}
}
) )
}
)
)
if ( GpuDriverHelper . supportsCustomDriverLoading ( ) ) {
add (
@ -164,13 +213,14 @@ class GamePropertiesFragment : Fragment() {
R . string . gpu_driver_manager ,
R . string . install_gpu_driver_description ,
R . drawable . ic_build ,
detailsFlow = driverViewModel . selectedDriverTitle
) {
detailsFlow = driverViewModel . selectedDriverTitle ,
action = {
val action = GamePropertiesFragmentDirections
. actionPerGamePropertiesFragmentToDriverManagerFragment ( args . game )
binding . root . findNavController ( ) . navigate ( action )
}
)
)
}
if ( ! args . game . isHomebrew ) {
@ -178,13 +228,14 @@ class GamePropertiesFragment : Fragment() {
SubmenuProperty (
R . string . add_ons ,
R . string . add_ons_description ,
R . drawable . ic_edit
) {
R . drawable . ic_edit ,
action = {
val action = GamePropertiesFragmentDirections
. actionPerGamePropertiesFragmentToAddonsFragment ( args . game )
binding . root . findNavController ( ) . navigate ( action )
}
)
)
add (
InstallableProperty (
R . string . save_data ,
@ -245,7 +296,7 @@ class GamePropertiesFragment : Fragment() {
R . string . clear_shader_cache ,
R . string . clear_shader_cache_description ,
R . drawable . ic_delete ,
{
details = {
if ( shaderCacheDir . exists ( ) ) {
val bytes = shaderCacheDir . walkTopDown ( ) . filter { it . isFile }
. map { it . length ( ) } . sum ( )
@ -253,8 +304,8 @@ class GamePropertiesFragment : Fragment() {
} else {
MemoryUtil . bytesToSizeUnit ( 0f )
}
}
) {
} ,
action = {
MessageDialogFragment . newInstance (
requireActivity ( ) ,
titleId = R . string . clear_shader_cache ,
@ -271,6 +322,7 @@ class GamePropertiesFragment : Fragment() {
) . show ( parentFragmentManager , MessageDialogFragment . TAG )
}
)
)
}
}
}
@ -284,6 +336,7 @@ class GamePropertiesFragment : Fragment() {
override fun onResume ( ) {
super . onResume ( )
driverViewModel . updateDriverNameForGame ( args . game )
reloadList ( )
}
private fun setInsets ( ) =
@ -420,4 +473,91 @@ class GamePropertiesFragment : Fragment() {
}
} . show ( parentFragmentManager , ProgressDialogFragment . TAG )
}
/ * *
* Imports an ini file from external storage to internal app directory and override per - game config
* /
private val importConfig = registerForActivityResult (
ActivityResultContracts . OpenDocument ( )
) { result ->
if ( result == null ) {
return @registerForActivityResult
}
val iniResult = FileUtil . copyUriToInternalStorage (
sourceUri = result ,
destinationParentPath =
DirectoryInitialization . userDirectory + " /config/custom/ " ,
destinationFilename = args . game . settingsName + " .ini "
)
if ( iniResult ?. exists ( ) == true ) {
Toast . makeText (
requireContext ( ) ,
getString ( R . string . import_success ) ,
Toast . LENGTH_SHORT
) . show ( )
homeViewModel . reloadPropertiesList ( true )
} else {
Toast . makeText (
requireContext ( ) ,
getString ( R . string . import_failed ) ,
Toast . LENGTH_SHORT
) . show ( )
}
}
/ * *
* Exports game ' s config ini to the specified location in external storage
* /
private val exportConfig = registerForActivityResult (
ActivityResultContracts . CreateDocument ( " text/ini " )
) { result ->
if ( result == null ) {
return @registerForActivityResult
}
ProgressDialogFragment . newInstance (
requireActivity ( ) ,
R . string . save_files_exporting ,
false
) { _ , _ ->
val configLocation = DirectoryInitialization . userDirectory +
" /config/custom/ " + args . game . settingsName + " .ini "
val iniResult = FileUtil . copyToExternalStorage (
sourcePath = configLocation ,
destUri = result
)
return @newInstance when ( iniResult ) {
TaskState . Completed -> getString ( R . string . export_success )
TaskState . Cancelled , TaskState . Failed -> getString ( R . string . export_failed )
}
} . show ( parentFragmentManager , ProgressDialogFragment . TAG )
}
private fun shareConfigFile ( configFile : File ) {
val file = DocumentFile . fromSingleUri (
requireContext ( ) ,
DocumentsContract . buildDocumentUri (
DocumentProvider . AUTHORITY ,
" ${DocumentProvider.ROOT_ID} / ${configFile} "
)
) !!
val intent = Intent ( Intent . ACTION_SEND )
. setDataAndType ( file . uri , FileUtil . TEXT_PLAIN )
. addFlags ( Intent . FLAG_GRANT_READ_URI_PERMISSION )
if ( file . exists ( ) ) {
intent . putExtra ( Intent . EXTRA_STREAM , file . uri )
startActivity ( Intent . createChooser ( intent , getText ( R . string . share_game_settings ) ) )
} else {
Toast . makeText (
requireContext ( ) ,
getText ( R . string . share_config_failed ) ,
Toast . LENGTH_SHORT
) . show ( )
}
}
}