Browse Source
rework the third, as ExternalContentProvider in patch_manager.cpp (less functions)
pull/2862/head
rework the third, as ExternalContentProvider in patch_manager.cpp (less functions)
pull/2862/head
23 changed files with 1444 additions and 57 deletions
-
53src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/ExternalContentAdapter.kt
-
105src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ExternalContentFragment.kt
-
14src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
-
56src/android/app/src/main/java/org/yuzu/yuzu_emu/model/ExternalContentViewModel.kt
-
24src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
-
8src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
-
24src/android/app/src/main/jni/native_config.cpp
-
42src/android/app/src/main/res/layout/card_external_content_dir.xml
-
69src/android/app/src/main/res/layout/fragment_external_content.xml
-
9src/android/app/src/main/res/values/strings.xml
-
1src/common/settings.h
-
311src/core/file_sys/patch_manager.cpp
-
12src/core/file_sys/patch_manager.h
-
425src/core/file_sys/registered_cache.cpp
-
49src/core/file_sys/registered_cache.h
-
37src/core/hle/service/filesystem/filesystem.cpp
-
8src/core/hle/service/filesystem/filesystem.h
-
19src/qt_common/config/qt_config.cpp
-
63src/yuzu/configuration/configure_filesystem.cpp
-
6src/yuzu/configuration/configure_filesystem.h
-
60src/yuzu/configuration/configure_filesystem.ui
-
101src/yuzu/configuration/configure_per_game_addons.cpp
-
5src/yuzu/configuration/configure_per_game_addons.h
@ -0,0 +1,53 @@ |
|||||
|
// 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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
// 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 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
// 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() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
<?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> |
||||
@ -0,0 +1,69 @@ |
|||||
|
<?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> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue