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