[android, gameProperties] Add support for sharing per-game config file #478
6 changed files with 332 additions and 88 deletions
|
@ -1,3 +1,6 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
@ -83,6 +86,34 @@ class GamePropertiesAdapter(
|
||||||
} else {
|
} else {
|
||||||
binding.details.setVisible(false)
|
binding.details.setVisible(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val hasVisibleActions = submenuProperty.secondaryActions?.any { it.isShown } == true
|
||||||
|
|
||||||
|
if (hasVisibleActions) {
|
||||||
|
binding.dividerSecondaryActions.setVisible(true)
|
||||||
|
binding.layoutSecondaryActions.setVisible(true)
|
||||||
|
|
||||||
|
submenuProperty.secondaryActions!!.forEach { secondaryAction ->
|
||||||
|
if (secondaryAction.isShown) {
|
||||||
|
val button = com.google.android.material.button.MaterialButton(
|
||||||
|
binding.root.context,
|
||||||
|
null,
|
||||||
|
com.google.android.material.R.attr.materialButtonOutlinedStyle
|
||||||
|
).apply {
|
||||||
|
setIconResource(secondaryAction.iconId)
|
||||||
|
iconSize = (18 * binding.root.context.resources.displayMetrics.density).toInt()
|
||||||
|
text = binding.root.context.getString(secondaryAction.descriptionId)
|
||||||
|
contentDescription = binding.root.context.getString(secondaryAction.descriptionId)
|
||||||
|
setOnClickListener { secondaryAction.action.invoke() }
|
||||||
|
}
|
||||||
|
binding.layoutSecondaryActions.addView(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.dividerSecondaryActions.setVisible(false)
|
||||||
|
binding.layoutSecondaryActions.setVisible(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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-FileCopyrightText: 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.ShortcutInfo
|
import android.content.pm.ShortcutInfo
|
||||||
import android.content.pm.ShortcutManager
|
import android.content.pm.ShortcutManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -14,6 +19,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
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.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
|
import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
|
||||||
import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
|
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.features.settings.model.Settings
|
||||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||||
import org.yuzu.yuzu_emu.model.GameProperty
|
import org.yuzu.yuzu_emu.model.GameProperty
|
||||||
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.InstallableProperty
|
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.SubmenuProperty
|
||||||
import org.yuzu.yuzu_emu.model.TaskState
|
import org.yuzu.yuzu_emu.model.TaskState
|
||||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
|
@ -137,25 +145,66 @@ class GamePropertiesFragment : Fragment() {
|
||||||
SubmenuProperty(
|
SubmenuProperty(
|
||||||
R.string.info,
|
R.string.info,
|
||||||
R.string.info_description,
|
R.string.info_description,
|
||||||
R.drawable.ic_info_outline
|
R.drawable.ic_info_outline,
|
||||||
) {
|
action = {
|
||||||
val action = GamePropertiesFragmentDirections
|
val action = GamePropertiesFragmentDirections
|
||||||
.actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
|
.actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
|
||||||
binding.root.findNavController().navigate(action)
|
binding.root.findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
add(
|
add(
|
||||||
SubmenuProperty(
|
SubmenuProperty(
|
||||||
R.string.preferences_settings,
|
R.string.preferences_settings,
|
||||||
R.string.per_game_settings_description,
|
R.string.per_game_settings_description,
|
||||||
R.drawable.ic_settings
|
R.drawable.ic_settings,
|
||||||
) {
|
action = {
|
||||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||||
args.game,
|
args.game,
|
||||||
Settings.MenuTag.SECTION_ROOT
|
Settings.MenuTag.SECTION_ROOT
|
||||||
)
|
)
|
||||||
binding.root.findNavController().navigate(action)
|
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()) {
|
if (GpuDriverHelper.supportsCustomDriverLoading()) {
|
||||||
|
@ -164,13 +213,14 @@ class GamePropertiesFragment : Fragment() {
|
||||||
R.string.gpu_driver_manager,
|
R.string.gpu_driver_manager,
|
||||||
R.string.install_gpu_driver_description,
|
R.string.install_gpu_driver_description,
|
||||||
R.drawable.ic_build,
|
R.drawable.ic_build,
|
||||||
detailsFlow = driverViewModel.selectedDriverTitle
|
detailsFlow = driverViewModel.selectedDriverTitle,
|
||||||
) {
|
action = {
|
||||||
val action = GamePropertiesFragmentDirections
|
val action = GamePropertiesFragmentDirections
|
||||||
.actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
|
.actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
|
||||||
binding.root.findNavController().navigate(action)
|
binding.root.findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.game.isHomebrew) {
|
if (!args.game.isHomebrew) {
|
||||||
|
@ -178,13 +228,14 @@ class GamePropertiesFragment : Fragment() {
|
||||||
SubmenuProperty(
|
SubmenuProperty(
|
||||||
R.string.add_ons,
|
R.string.add_ons,
|
||||||
R.string.add_ons_description,
|
R.string.add_ons_description,
|
||||||
R.drawable.ic_edit
|
R.drawable.ic_edit,
|
||||||
) {
|
action = {
|
||||||
val action = GamePropertiesFragmentDirections
|
val action = GamePropertiesFragmentDirections
|
||||||
.actionPerGamePropertiesFragmentToAddonsFragment(args.game)
|
.actionPerGamePropertiesFragmentToAddonsFragment(args.game)
|
||||||
binding.root.findNavController().navigate(action)
|
binding.root.findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
add(
|
add(
|
||||||
InstallableProperty(
|
InstallableProperty(
|
||||||
R.string.save_data,
|
R.string.save_data,
|
||||||
|
@ -245,7 +296,7 @@ class GamePropertiesFragment : Fragment() {
|
||||||
R.string.clear_shader_cache,
|
R.string.clear_shader_cache,
|
||||||
R.string.clear_shader_cache_description,
|
R.string.clear_shader_cache_description,
|
||||||
R.drawable.ic_delete,
|
R.drawable.ic_delete,
|
||||||
{
|
details = {
|
||||||
if (shaderCacheDir.exists()) {
|
if (shaderCacheDir.exists()) {
|
||||||
val bytes = shaderCacheDir.walkTopDown().filter { it.isFile }
|
val bytes = shaderCacheDir.walkTopDown().filter { it.isFile }
|
||||||
.map { it.length() }.sum()
|
.map { it.length() }.sum()
|
||||||
|
@ -253,8 +304,8 @@ class GamePropertiesFragment : Fragment() {
|
||||||
} else {
|
} else {
|
||||||
MemoryUtil.bytesToSizeUnit(0f)
|
MemoryUtil.bytesToSizeUnit(0f)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
) {
|
action = {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
titleId = R.string.clear_shader_cache,
|
titleId = R.string.clear_shader_cache,
|
||||||
|
@ -271,6 +322,7 @@ class GamePropertiesFragment : Fragment() {
|
||||||
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -284,6 +336,7 @@ class GamePropertiesFragment : Fragment() {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
driverViewModel.updateDriverNameForGame(args.game)
|
driverViewModel.updateDriverNameForGame(args.game)
|
||||||
|
reloadList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setInsets() =
|
private fun setInsets() =
|
||||||
|
@ -420,4 +473,91 @@ class GamePropertiesFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
}.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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
@ -24,9 +27,17 @@ data class SubmenuProperty(
|
||||||
override val iconId: Int,
|
override val iconId: Int,
|
||||||
val details: (() -> String)? = null,
|
val details: (() -> String)? = null,
|
||||||
val detailsFlow: StateFlow<String>? = null,
|
val detailsFlow: StateFlow<String>? = null,
|
||||||
val action: () -> Unit
|
val action: () -> Unit,
|
||||||
|
val secondaryActions: List<SubMenuPropertySecondaryAction>? = null
|
||||||
) : GameProperty
|
) : GameProperty
|
||||||
|
|
||||||
|
data class SubMenuPropertySecondaryAction(
|
||||||
|
val isShown : Boolean,
|
||||||
|
val descriptionId: Int,
|
||||||
|
val iconId: Int,
|
||||||
|
val action: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
data class InstallableProperty(
|
data class InstallableProperty(
|
||||||
override val titleId: Int,
|
override val titleId: Int,
|
||||||
override val descriptionId: Int,
|
override val descriptionId: Int,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import java.net.URLDecoder
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.features.DocumentProvider
|
||||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||||
import org.yuzu.yuzu_emu.model.TaskState
|
import org.yuzu.yuzu_emu.model.TaskState
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
|
@ -291,6 +292,39 @@ object FileUtil {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a file from internal appdata storage to an external Uri.
|
||||||
|
*/
|
||||||
|
fun copyToExternalStorage(
|
||||||
|
sourcePath: String,
|
||||||
|
destUri: Uri,
|
||||||
|
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
|
||||||
|
): TaskState {
|
||||||
|
try {
|
||||||
|
val totalBytes = getFileSize(sourcePath)
|
||||||
|
var progressBytes = 0L
|
||||||
|
val inputStream = getInputStream(sourcePath)
|
||||||
|
BufferedInputStream(inputStream).use { bis ->
|
||||||
|
context.contentResolver.openOutputStream(destUri, "wt")?.use { outputStream ->
|
||||||
|
val buffer = ByteArray(1024 * 4)
|
||||||
|
var len: Int
|
||||||
|
while (bis.read(buffer).also { len = it } != -1) {
|
||||||
|
if (progressCallback.invoke(totalBytes, progressBytes)) {
|
||||||
|
return TaskState.Cancelled
|
||||||
|
}
|
||||||
|
outputStream.write(buffer, 0, len)
|
||||||
|
progressBytes += len
|
||||||
|
}
|
||||||
|
outputStream.flush()
|
||||||
|
} ?: return TaskState.Failed
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.error("[FileUtil] Failed exporting file - ${e.message}")
|
||||||
|
return TaskState.Failed
|
||||||
|
}
|
||||||
|
return TaskState.Completed
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the given zip file into the given directory.
|
* Extracts the given zip file into the given directory.
|
||||||
* @param path String representation of a [Uri] or a typical path delimited by '/'
|
* @param path String representation of a [Uri] or a typical path delimited by '/'
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -69,4 +74,22 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.divider.MaterialDivider
|
||||||
|
android:id="@+id/dividerSecondaryActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.ChipGroup
|
||||||
|
android:id="@+id/layoutSecondaryActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:singleLine="false"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
|
@ -677,6 +677,7 @@
|
||||||
<string name="fetch">Fetch</string>
|
<string name="fetch">Fetch</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
|
<string name="import_success">Imported successfully</string>
|
||||||
<string name="export_success">Exported successfully</string>
|
<string name="export_success">Exported successfully</string>
|
||||||
<string name="start">Start</string>
|
<string name="start">Start</string>
|
||||||
<string name="clear">Clear</string>
|
<string name="clear">Clear</string>
|
||||||
|
@ -791,6 +792,10 @@
|
||||||
<string name="verify_no_result">Integrity verification couldn\'t be performed</string>
|
<string name="verify_no_result">Integrity verification couldn\'t be performed</string>
|
||||||
<string name="verify_no_result_description">File contents were not checked for validity</string>
|
<string name="verify_no_result_description">File contents were not checked for validity</string>
|
||||||
<string name="verification_failed_for">Verification failed for the following files:\n%1$s</string>
|
<string name="verification_failed_for">Verification failed for the following files:\n%1$s</string>
|
||||||
|
<string name="share_game_settings">Share Config</string>
|
||||||
|
<string name="import_config">Import Config</string>
|
||||||
|
<string name="export_config">Export Config</string>
|
||||||
|
<string name="share_config_failed">Failed to share configuration file</string>
|
||||||
|
|
||||||
<!-- ROM loading errors -->
|
<!-- ROM loading errors -->
|
||||||
<string name="loader_error_encrypted">Your ROM is encrypted</string>
|
<string name="loader_error_encrypted">Your ROM is encrypted</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue