put external content in "Manage game folders" and show a modal. Also handles logic for android now.
This commit is contained in:
parent
5d1035a203
commit
fd61d098ab
|
|
@ -1,53 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.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.CardExternalContentDirBinding
|
||||
import org.yuzu.yuzu_emu.model.ExternalContentViewModel
|
||||
|
||||
class ExternalContentAdapter(
|
||||
private val viewModel: ExternalContentViewModel
|
||||
) : ListAdapter<String, ExternalContentAdapter.DirectoryViewHolder>(
|
||||
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DirectoryViewHolder {
|
||||
val binding = CardExternalContentDirBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return DirectoryViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DirectoryViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class DirectoryViewHolder(val binding: CardExternalContentDirBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(path: String) {
|
||||
binding.textPath.text = path
|
||||
binding.buttonRemove.setOnClickListener {
|
||||
viewModel.removeDirectory(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,10 @@ import android.net.Uri
|
|||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
||||
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
||||
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
|
||||
|
|
@ -31,6 +33,12 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
|
|||
path.text = Uri.parse(model.uriString).path
|
||||
path.marquee()
|
||||
|
||||
// Set type indicator, shows below folder name, to see if DLC or Games
|
||||
typeIndicator.text = when (model.type) {
|
||||
DirectoryType.GAME -> activity.getString(R.string.games)
|
||||
DirectoryType.EXTERNAL_CONTENT -> activity.getString(R.string.external_content)
|
||||
}
|
||||
|
||||
buttonEdit.setOnClickListener {
|
||||
GameFolderPropertiesDialogFragment.newInstance(model)
|
||||
.show(
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.adapters.ExternalContentAdapter
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentExternalContentBinding
|
||||
import org.yuzu.yuzu_emu.model.ExternalContentViewModel
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
|
||||
import org.yuzu.yuzu_emu.utils.collect
|
||||
|
||||
class ExternalContentFragment : Fragment() {
|
||||
private var _binding: FragmentExternalContentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private val externalContentViewModel: ExternalContentViewModel by activityViewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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 = FragmentExternalContentBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
binding.toolbarExternalContent.setNavigationOnClickListener {
|
||||
binding.root.findNavController().popBackStack()
|
||||
}
|
||||
|
||||
binding.listExternalDirs.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = ExternalContentAdapter(externalContentViewModel)
|
||||
}
|
||||
|
||||
externalContentViewModel.directories.collect(viewLifecycleOwner) { dirs ->
|
||||
(binding.listExternalDirs.adapter as ExternalContentAdapter).submitList(dirs)
|
||||
binding.textEmpty.visibility = if (dirs.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
val mainActivity = requireActivity() as MainActivity
|
||||
binding.buttonAdd.setOnClickListener {
|
||||
mainActivity.getExternalContentDirectory.launch(null)
|
||||
}
|
||||
|
||||
setInsets()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
binding.toolbarExternalContent.updateMargins(left = leftInsets, right = rightInsets)
|
||||
|
||||
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||
binding.buttonAdd.updateMargins(
|
||||
left = leftInsets + fabSpacing,
|
||||
right = rightInsets + fabSpacing,
|
||||
bottom = barInsets.bottom + fabSpacing
|
||||
)
|
||||
|
||||
binding.listExternalDirs.updateMargins(left = leftInsets, right = rightInsets)
|
||||
|
||||
binding.listExternalDirs.updatePadding(
|
||||
bottom = barInsets.bottom +
|
||||
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||
)
|
||||
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,13 @@ package org.yuzu.yuzu_emu.fragments
|
|||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
||||
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
|
@ -25,14 +27,18 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
|
|||
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
||||
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
||||
|
||||
// Restore checkbox state
|
||||
binding.deepScanSwitch.isChecked =
|
||||
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
||||
// Hide deepScan for external content, do automatically
|
||||
if (gameDir.type == DirectoryType.EXTERNAL_CONTENT) {
|
||||
binding.deepScanSwitch.visibility = View.GONE
|
||||
} else {
|
||||
// Restore checkbox state for game dirs
|
||||
binding.deepScanSwitch.isChecked =
|
||||
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
||||
|
||||
// Ensure that we can get the checkbox state even if the view is destroyed
|
||||
deepScan = binding.deepScanSwitch.isChecked
|
||||
binding.deepScanSwitch.setOnClickListener {
|
||||
deepScan = binding.deepScanSwitch.isChecked
|
||||
binding.deepScanSwitch.setOnClickListener {
|
||||
deepScan = binding.deepScanSwitch.isChecked
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
|
|
@ -41,8 +47,10 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
|
|||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
||||
if (folderIndex != -1) {
|
||||
gamesViewModel.folders.value[folderIndex].deepScan =
|
||||
binding.deepScanSwitch.isChecked
|
||||
if (gameDir.type == DirectoryType.GAME) {
|
||||
gamesViewModel.folders.value[folderIndex].deepScan =
|
||||
binding.deepScanSwitch.isChecked
|
||||
}
|
||||
gamesViewModel.updateGameDirs()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import kotlinx.coroutines.launch
|
|||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.adapters.FolderAdapter
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
|
||||
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
|
|
@ -73,7 +75,25 @@ class GameFoldersFragment : Fragment() {
|
|||
|
||||
val mainActivity = requireActivity() as MainActivity
|
||||
binding.buttonAdd.setOnClickListener {
|
||||
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||
// Show a model to choose between Game and External Content
|
||||
val options = arrayOf(
|
||||
getString(R.string.games),
|
||||
getString(R.string.external_content)
|
||||
)
|
||||
|
||||
android.app.AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.add_folders)
|
||||
.setItems(options) { _, which ->
|
||||
when (which) {
|
||||
0 -> { // Game Folder
|
||||
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||
}
|
||||
1 -> { // External Content Folder
|
||||
mainActivity.getExternalContentDirectory.launch(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
setInsets()
|
||||
|
|
|
|||
|
|
@ -179,17 +179,6 @@ class HomeSettingsFragment : Fragment() {
|
|||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.manage_external_content,
|
||||
R.string.manage_external_content_description,
|
||||
R.drawable.ic_add,
|
||||
{
|
||||
binding.root.findNavController()
|
||||
.navigate(R.id.action_homeSettingsFragment_to_externalContentFragment)
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.verify_installed_content,
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
class ExternalContentViewModel : ViewModel() {
|
||||
private val _directories = MutableStateFlow(listOf<String>())
|
||||
val directories: StateFlow<List<String>> get() = _directories
|
||||
|
||||
init {
|
||||
loadDirectories()
|
||||
}
|
||||
|
||||
private fun loadDirectories() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
_directories.value = NativeConfig.getExternalContentDirs().toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addDirectory(dir: DocumentFile) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val path = dir.uri.toString()
|
||||
val currentDirs = _directories.value.toMutableList()
|
||||
if (!currentDirs.contains(path)) {
|
||||
currentDirs.add(path)
|
||||
NativeConfig.setExternalContentDirs(currentDirs.toTypedArray())
|
||||
_directories.value = currentDirs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeDirectory(path: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val currentDirs = _directories.value.toMutableList()
|
||||
currentDirs.remove(path)
|
||||
NativeConfig.setExternalContentDirs(currentDirs.toTypedArray())
|
||||
_directories.value = currentDirs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,5 +9,14 @@ import kotlinx.parcelize.Parcelize
|
|||
@Parcelize
|
||||
data class GameDir(
|
||||
val uriString: String,
|
||||
var deepScan: Boolean
|
||||
) : Parcelable
|
||||
var deepScan: Boolean,
|
||||
val type: DirectoryType = DirectoryType.GAME
|
||||
) : Parcelable {
|
||||
// Needed for JNI backward compatability
|
||||
constructor(uriString: String, deepScan: Boolean) : this(uriString, deepScan, DirectoryType.GAME)
|
||||
}
|
||||
|
||||
enum class DirectoryType {
|
||||
GAME,
|
||||
EXTERNAL_CONTENT
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class GamesViewModel : ViewModel() {
|
|||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||
NativeLibrary.reloadKeys()
|
||||
|
||||
getGameDirs()
|
||||
getGameDirsAndExternalContent()
|
||||
reloadGames(directoriesChanged = false, firstStartup = true)
|
||||
}
|
||||
|
||||
|
|
@ -144,11 +144,18 @@ class GamesViewModel : ViewModel() {
|
|||
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
NativeConfig.addGameDir(gameDir)
|
||||
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
.getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||
|
||||
getGameDirs(!isFirstTimeSetup)
|
||||
when (gameDir.type) {
|
||||
DirectoryType.GAME -> {
|
||||
NativeConfig.addGameDir(gameDir)
|
||||
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
.getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||
getGameDirsAndExternalContent(!isFirstTimeSetup)
|
||||
}
|
||||
DirectoryType.EXTERNAL_CONTENT -> {
|
||||
addExternalContentDir(gameDir.uriString)
|
||||
getGameDirsAndExternalContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (savedFromGameFragment) {
|
||||
|
|
@ -168,8 +175,15 @@ class GamesViewModel : ViewModel() {
|
|||
val removedDirIndex = gameDirs.indexOf(gameDir)
|
||||
if (removedDirIndex != -1) {
|
||||
gameDirs.removeAt(removedDirIndex)
|
||||
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||
getGameDirs()
|
||||
when (gameDir.type) {
|
||||
DirectoryType.GAME -> {
|
||||
NativeConfig.setGameDirs(gameDirs.filter { it.type == DirectoryType.GAME }.toTypedArray())
|
||||
}
|
||||
DirectoryType.EXTERNAL_CONTENT -> {
|
||||
removeExternalContentDir(gameDir.uriString)
|
||||
}
|
||||
}
|
||||
getGameDirsAndExternalContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -177,15 +191,16 @@ class GamesViewModel : ViewModel() {
|
|||
fun updateGameDirs() =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
NativeConfig.setGameDirs(_folders.value.toTypedArray())
|
||||
getGameDirs()
|
||||
val gameDirs = _folders.value.filter { it.type == DirectoryType.GAME }
|
||||
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||
getGameDirsAndExternalContent()
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenGameFoldersFragment() =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
getGameDirs()
|
||||
getGameDirsAndExternalContent()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,16 +208,34 @@ class GamesViewModel : ViewModel() {
|
|||
NativeConfig.saveGlobalConfig()
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
getGameDirs(true)
|
||||
getGameDirsAndExternalContent(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGameDirs(reloadList: Boolean = false) {
|
||||
val gameDirs = NativeConfig.getGameDirs()
|
||||
_folders.value = gameDirs.toMutableList()
|
||||
private fun getGameDirsAndExternalContent(reloadList: Boolean = false) {
|
||||
val gameDirs = NativeConfig.getGameDirs().toMutableList()
|
||||
val externalContentDirs = NativeConfig.getExternalContentDirs().map {
|
||||
GameDir(it, false, DirectoryType.EXTERNAL_CONTENT)
|
||||
}
|
||||
gameDirs.addAll(externalContentDirs)
|
||||
_folders.value = gameDirs
|
||||
if (reloadList) {
|
||||
reloadGames(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addExternalContentDir(path: String) {
|
||||
val currentDirs = NativeConfig.getExternalContentDirs().toMutableList()
|
||||
if (!currentDirs.contains(path)) {
|
||||
currentDirs.add(path)
|
||||
NativeConfig.setExternalContentDirs(currentDirs.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeExternalContentDir(path: String) {
|
||||
val currentDirs = NativeConfig.getExternalContentDirs().toMutableList()
|
||||
currentDirs.remove(path)
|
||||
NativeConfig.setExternalContentDirs(currentDirs.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -425,14 +425,18 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
)
|
||||
|
||||
val uriString = result.toString()
|
||||
val externalContentViewModel by viewModels<org.yuzu.yuzu_emu.model.ExternalContentViewModel>()
|
||||
externalContentViewModel.addDirectory(DocumentFile.fromTreeUri(this, result)!!)
|
||||
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
||||
if (folder != null) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.folder_already_added,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.add_directory_success,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
val externalContentDir = org.yuzu.yuzu_emu.model.GameDir(uriString, false, org.yuzu.yuzu_emu.model.DirectoryType.EXTERNAL_CONTENT)
|
||||
gamesViewModel.addFolder(externalContentDir, savedFromGameFragment = false)
|
||||
}
|
||||
|
||||
val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||
|
|
|
|||
|
|
@ -49,6 +49,17 @@ object GameHelper {
|
|||
// Remove previous filesystem provider information so we can get up to date version info
|
||||
NativeLibrary.clearFilesystemProvider()
|
||||
|
||||
// Scan External Content directories and register all NSP/XCI files
|
||||
val externalContentDirs = NativeConfig.getExternalContentDirs()
|
||||
for (externalDir in externalContentDirs) {
|
||||
if (externalDir.isNotEmpty()) {
|
||||
val externalDirUri = externalDir.toUri()
|
||||
if (FileUtil.isTreeUriValid(externalDirUri)) {
|
||||
scanExternalContentRecursive(FileUtil.listFiles(externalDirUri), 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val badDirs = mutableListOf<Int>()
|
||||
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||
val gameDirUri = gameDir.uriString.toUri()
|
||||
|
|
@ -88,6 +99,33 @@ object GameHelper {
|
|||
return games.toList()
|
||||
}
|
||||
|
||||
// File extensions considered as external content, buuut should
|
||||
// be done better imo.
|
||||
private val externalContentExtensions = setOf("nsp", "xci")
|
||||
|
||||
private fun scanExternalContentRecursive(
|
||||
files: Array<MinimalDocumentFile>,
|
||||
depth: Int
|
||||
) {
|
||||
if (depth <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
files.forEach {
|
||||
if (it.isDirectory) {
|
||||
scanExternalContentRecursive(
|
||||
FileUtil.listFiles(it.uri),
|
||||
depth - 1
|
||||
)
|
||||
} else {
|
||||
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||
if (externalContentExtensions.contains(extension)) {
|
||||
NativeLibrary.addFileToFilesystemProvider(it.uri.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addGamesRecursive(
|
||||
games: MutableList<Game>,
|
||||
files: Array<MinimalDocumentFile>,
|
||||
|
|
|
|||
|
|
@ -210,6 +210,40 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
|||
return;
|
||||
}
|
||||
|
||||
const auto extension = Common::ToLower(filepath.substr(filepath.find_last_of('.') + 1));
|
||||
|
||||
if (extension == "nsp") {
|
||||
auto nsp = std::make_shared<FileSys::NSP>(file);
|
||||
if (nsp->GetStatus() == Loader::ResultStatus::Success) {
|
||||
for (const auto& title : nsp->GetNCAs()) {
|
||||
for (const auto& entry : title.second) {
|
||||
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
||||
entry.second->GetBaseFile());
|
||||
LOG_DEBUG(Frontend, "Added NSP entry - TitleID: {:016X}, TitleType: {}, ContentType: {}",
|
||||
title.first, static_cast<int>(entry.first.first), static_cast<int>(entry.first.second));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle XCI files
|
||||
if (extension == "xci") {
|
||||
FileSys::XCI xci{file};
|
||||
if (xci.GetStatus() == Loader::ResultStatus::Success) {
|
||||
const auto nsp = xci.GetSecurePartitionNSP();
|
||||
if (nsp) {
|
||||
for (const auto& title : nsp->GetNCAs()) {
|
||||
for (const auto& entry : title.second) {
|
||||
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
||||
entry.second->GetBaseFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto loader = Loader::GetLoader(m_system, file);
|
||||
if (!loader) {
|
||||
return;
|
||||
|
|
@ -226,17 +260,6 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
|||
m_manual_provider->AddEntry(FileSys::TitleType::Application,
|
||||
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
||||
program_id, file);
|
||||
} else if (res2 == Loader::ResultStatus::Success &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
||||
const auto nsp = file_type == Loader::FileType::NSP
|
||||
? std::make_shared<FileSys::NSP>(file)
|
||||
: FileSys::XCI{file}.GetSecurePartitionNSP();
|
||||
for (const auto& title : nsp->GetNCAs()) {
|
||||
for (const auto& entry : title.second) {
|
||||
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
||||
entry.second->GetBaseFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_path"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_remove"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/remove_external_content_dir" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
|
|
@ -23,12 +23,25 @@
|
|||
android:layout_gravity="center_vertical|start"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/type_indicator"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/select_gpu_driver_default" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/type_indicator"
|
||||
style="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/path"
|
||||
tools:text="Games" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_layout"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
<?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_external_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_external_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:liftOnScrollTargetViewId="@id/list_external_dirs">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_external_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/external_content_directories" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list_external_dirs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:defaultFocusHighlightEnabled="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:text="@string/no_external_content_dirs"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/button_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:contentDescription="@string/add_external_content_dir"
|
||||
app:srcCompat="@drawable/ic_add"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -186,15 +186,4 @@
|
|||
app:nullable="true"
|
||||
android:defaultValue="@null" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/externalContentFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.ExternalContentFragment"
|
||||
android:label="ExternalContentFragment" />
|
||||
<action
|
||||
android:id="@+id/action_global_externalContentFragment"
|
||||
app:destination="@id/externalContentFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_homeSettingsFragment_to_externalContentFragment"
|
||||
app:destination="@id/externalContentFragment" />
|
||||
</navigation>
|
||||
|
|
|
|||
|
|
@ -1746,12 +1746,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
</string>
|
||||
|
||||
<string name="manage_external_content">Manage External Content</string>
|
||||
<string name="manage_external_content_description">Configure directories for loading DLCs/Updates without NAND installation</string>
|
||||
<string name="external_content_directories">External Content Directories</string>
|
||||
<string name="add_external_content_dir">Add Directory</string>
|
||||
<string name="remove_external_content_dir">Remove</string>
|
||||
<string name="external_content_description">Add directories containing NSP/XCI files with DLCs and Updates. These will be loaded without installing to NAND, saving disk space.</string>
|
||||
<string name="no_external_content_dirs">No external content directories configured.\n\nAdd a directory to load DLCs/Updates without NAND installation.</string>
|
||||
<string name="external_content">External Content</string>
|
||||
<string name="add_folders">Add Folder</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in New Issue