diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/ExternalContentAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/ExternalContentAdapter.kt deleted file mode 100644 index b28ae73a9b..0000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/ExternalContentAdapter.kt +++ /dev/null @@ -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( - 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() { - override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { - return oldItem == newItem - } - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt index 5cbd15d2ac..63ee2be2b9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt @@ -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( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ExternalContentFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ExternalContentFragment.kt deleted file mode 100644 index f493ab98ac..0000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ExternalContentFragment.kt +++ /dev/null @@ -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 - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt index 1ea1e036e6..03bfa179be 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt @@ -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(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() } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt index 87b1533408..28ed773458 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt @@ -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() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 803db6e70e..b267a597e1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -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, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/ExternalContentViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/ExternalContentViewModel.kt deleted file mode 100644 index 6a7943308d..0000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/ExternalContentViewModel.kt +++ /dev/null @@ -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()) - val directories: StateFlow> 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 - } - } - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt index 274bc1c7bc..af7a71c66e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt @@ -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 +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index ae5f8f89de..0564a64afe 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -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()) + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index dced3e7ab8..8edec4ff46 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -425,14 +425,18 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ) val uriString = result.toString() - val externalContentViewModel by viewModels() - externalContentViewModel.addDirectory(DocumentFile.fromTreeUri(this, result)!!) - - Toast.makeText( - applicationContext, - R.string.add_directory_success, - Toast.LENGTH_SHORT - ).show() + 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 + } + + 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 -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index e27bc94696..19fec60709 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -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() 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, + 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, files: Array, diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 37f195fe15..9f86d9d99f 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -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(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(entry.first.first), static_cast(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(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()); - } - } } } diff --git a/src/android/app/src/main/res/layout/card_external_content_dir.xml b/src/android/app/src/main/res/layout/card_external_content_dir.xml deleted file mode 100644 index dcb92170d8..0000000000 --- a/src/android/app/src/main/res/layout/card_external_content_dir.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/android/app/src/main/res/layout/card_folder.xml b/src/android/app/src/main/res/layout/card_folder.xml index e3a5f1a867..517063e7ac 100644 --- a/src/android/app/src/main/res/layout/card_folder.xml +++ b/src/android/app/src/main/res/layout/card_folder.xml @@ -11,7 +11,7 @@ @@ -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" /> + + - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index 7bb1ebb2d8..e538002a70 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -186,15 +186,4 @@ app:nullable="true" android:defaultValue="@null" /> - - - - diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 546cc89015..6a7bbe5de9 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -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. - Manage External Content - Configure directories for loading DLCs/Updates without NAND installation - External Content Directories - Add Directory - Remove - Add directories containing NSP/XCI files with DLCs and Updates. These will be loaded without installing to NAND, saving disk space. - No external content directories configured.\n\nAdd a directory to load DLCs/Updates without NAND installation. + External Content + Add Folder