diff options
author | Charles Lombardo <clombardo169@gmail.com> | 2023-08-24 22:11:08 +0200 |
---|---|---|
committer | Charles Lombardo <clombardo169@gmail.com> | 2023-08-30 01:42:42 +0200 |
commit | fd5c7b21ddc0e40df049773d9106e349072352f3 (patch) | |
tree | 65ddb2544937e6efc9141050870b511d514919f7 | |
parent | android: Implement paired settings (diff) | |
download | yuzu-fd5c7b21ddc0e40df049773d9106e349072352f3.tar yuzu-fd5c7b21ddc0e40df049773d9106e349072352f3.tar.gz yuzu-fd5c7b21ddc0e40df049773d9106e349072352f3.tar.bz2 yuzu-fd5c7b21ddc0e40df049773d9106e349072352f3.tar.lz yuzu-fd5c7b21ddc0e40df049773d9106e349072352f3.tar.xz yuzu-fd5c7b21ddc0e40df049773d9106e349072352f3.tar.zst yuzu-fd5c7b21ddc0e40df049773d9106e349072352f3.zip |
8 files changed, 372 insertions, 1 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index ff1e64e0a..f5eba1222 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -12,6 +12,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import androidx.recyclerview.widget.AsyncDifferConfig @@ -37,7 +38,7 @@ import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* import org.yuzu.yuzu_emu.model.SettingsViewModel class SettingsAdapter( - private val fragment: SettingsFragment, + private val fragment: Fragment, private val context: Context ) : ListAdapter<SettingsItem, SettingViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), DialogInterface.OnClickListener { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 5890b0642..0ea587a88 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -13,12 +13,14 @@ 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.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.transition.MaterialSharedAxis import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.model.SettingsViewModel class SettingsFragment : Fragment() { @@ -84,11 +86,43 @@ class SettingsFragment : Fragment() { } } + settingsViewModel.isUsingSearch.observe(viewLifecycleOwner) { + if (it) { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + } else { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + } + } + + if (args.menuTag == SettingsFile.FILE_NAME_CONFIG) { + binding.toolbarSettings.inflateMenu(R.menu.menu_settings) + binding.toolbarSettings.setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_search -> { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + view.findNavController() + .navigate(R.id.action_settingsFragment_to_settingsSearchFragment) + true + } + + else -> false + } + } + } + presenter.onViewCreated() setInsets() } + override fun onResume() { + super.onResume() + settingsViewModel.setIsUsingSearch(false) + } + override fun onDetach() { super.onDetach() settingsAdapter?.closeDialog() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt new file mode 100644 index 000000000..4f93db4ad --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.divider.MaterialDividerItemDecoration +import com.google.android.material.transition.MaterialSharedAxis +import info.debatty.java.stringsimilarity.Cosine +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.NativeConfig + +class SettingsSearchFragment : Fragment() { + private var _binding: FragmentSettingsSearchBinding? = null + private val binding get() = _binding!! + + private var settingsAdapter: SettingsAdapter? = null + + private val settingsViewModel: SettingsViewModel by activityViewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSettingsSearchBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settingsViewModel.setIsUsingSearch(true) + + if (savedInstanceState != null) { + binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT)) + } + + settingsAdapter = SettingsAdapter(this, requireContext()) + + val dividerDecoration = MaterialDividerItemDecoration( + requireContext(), + LinearLayoutManager.VERTICAL + ) + dividerDecoration.isLastItemDecorated = false + binding.settingsList.apply { + adapter = settingsAdapter + layoutManager = LinearLayoutManager(requireContext()) + addItemDecoration(dividerDecoration) + } + + focusSearch() + + binding.backButton.setOnClickListener { settingsViewModel.setShouldNavigateBack(true) } + binding.searchBackground.setOnClickListener { focusSearch() } + binding.clearButton.setOnClickListener { binding.searchText.setText("") } + binding.searchText.doOnTextChanged { _, _, _, _ -> + search() + binding.settingsList.smoothScrollToPosition(0) + } + settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) { + if (it) { + settingsViewModel.setShouldReloadSettingsList(false) + search() + } + } + + search() + + setInsets() + } + + override fun onDetach() { + super.onDetach() + settingsAdapter?.closeDialog() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(SEARCH_TEXT, binding.searchText.text.toString()) + } + + private fun search() { + val searchTerm = binding.searchText.text.toString().lowercase() + binding.clearButton.visibility = + if (searchTerm.isEmpty()) View.INVISIBLE else View.VISIBLE + if (searchTerm.isEmpty()) { + binding.noResultsView.visibility = View.VISIBLE + settingsAdapter?.submitList(emptyList()) + return + } + + val baseList = SettingsItem.settingsItems + val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) + val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> + val title = getString(item.value.nameId).lowercase() + val similarity = similarityAlgorithm.similarity(searchTerm, title) + if (similarity > 0.08) { + Pair(similarity, item) + } else { + null + } + }.sortedByDescending { it.first }.mapNotNull { + val item = it.second.value + val pairedSettingKey = item.setting.pairedSettingKey + val optionalSetting: SettingsItem? = if (pairedSettingKey.isNotEmpty()) { + val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false) + if (pairedSettingValue) it.second.value else null + } else { + it.second.value + } + optionalSetting + } + settingsAdapter?.submitList(sortedList) + binding.noResultsView.visibility = + if (sortedList.isEmpty()) View.VISIBLE else View.INVISIBLE + } + + private fun focusSearch() { + binding.searchText.requestFocus() + val imm = requireActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT) + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) + val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge) + val topMargin = resources.getDimensionPixelSize(R.dimen.spacing_chip) + + 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.settingsList.updatePadding(bottom = barInsets.bottom + extraListSpacing) + binding.frameSearch.updatePadding( + left = leftInsets + sideMargin, + top = barInsets.top + topMargin, + right = rightInsets + sideMargin + ) + binding.noResultsView.updatePadding( + left = leftInsets, + right = rightInsets, + bottom = barInsets.bottom + ) + + val mlpSettingsList = binding.settingsList.layoutParams as ViewGroup.MarginLayoutParams + mlpSettingsList.leftMargin = leftInsets + sideMargin + mlpSettingsList.rightMargin = rightInsets + sideMargin + binding.settingsList.layoutParams = mlpSettingsList + + val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams + mlpDivider.leftMargin = leftInsets + sideMargin + mlpDivider.rightMargin = rightInsets + sideMargin + binding.divider.layoutParams = mlpDivider + + windowInsets + } + + companion object { + const val SEARCH_TEXT = "SearchText" + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt index 6f2276293..a0cb7225f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt @@ -27,6 +27,9 @@ class SettingsViewModel : ViewModel() { private val _shouldReloadSettingsList = MutableLiveData(false) val shouldReloadSettingsList: LiveData<Boolean> get() = _shouldReloadSettingsList + private val _isUsingSearch = MutableLiveData(false) + val isUsingSearch: LiveData<Boolean> get() = _isUsingSearch + fun setToolbarTitle(value: String) { _toolbarTitle.value = value } @@ -47,6 +50,10 @@ class SettingsViewModel : ViewModel() { _shouldReloadSettingsList.value = value } + fun setIsUsingSearch(value: Boolean) { + _isUsingSearch.value = value + } + fun clear() { game = null shouldSave = false diff --git a/src/android/app/src/main/res/layout/fragment_settings_search.xml b/src/android/app/src/main/res/layout/fragment_settings_search.xml new file mode 100644 index 000000000..c779ed2fc --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_settings_search.xml @@ -0,0 +1,120 @@ +<?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" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <RelativeLayout + android:id="@+id/relativeLayout" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/divider"> + + <LinearLayout + android:id="@+id/no_results_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical"> + + <ImageView + android:id="@+id/icon_no_results" + android:layout_width="match_parent" + android:layout_height="80dp" + android:src="@drawable/ic_search" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/notice_text" + style="@style/TextAppearance.Material3.TitleLarge" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:paddingTop="8dp" + android:text="@string/search_settings" + tools:visibility="visible" /> + + </LinearLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/settings_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" /> + + </RelativeLayout> + + <FrameLayout + android:id="@+id/frame_search" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/search_background" + style="?attr/materialCardViewFilledStyle" + android:layout_width="match_parent" + android:layout_height="56dp" + app:cardCornerRadius="28dp"> + + <LinearLayout + android:id="@+id/search_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginEnd="56dp" + android:orientation="horizontal"> + + <Button + android:id="@+id/back_button" + style="?attr/materialIconButtonFilledTonalStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="8dp" + app:backgroundTint="@android:color/transparent" + app:icon="@drawable/ic_back" /> + + <EditText + android:id="@+id/search_text" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/transparent" + android:hint="@string/search_settings" + android:imeOptions="flagNoFullscreen" + android:inputType="text" + android:maxLines="1" /> + + </LinearLayout> + + <Button + android:id="@+id/clear_button" + style="?attr/materialIconButtonFilledTonalStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|end" + android:layout_marginEnd="8dp" + android:visibility="invisible" + app:backgroundTint="@android:color/transparent" + app:icon="@drawable/ic_clear" + tools:visibility="visible" /> + + </com.google.android.material.card.MaterialCardView> + + </FrameLayout> + + <com.google.android.material.divider.MaterialDivider + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/frame_search" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/src/android/app/src/main/res/menu/menu_settings.xml b/src/android/app/src/main/res/menu/menu_settings.xml new file mode 100644 index 000000000..21501a471 --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_settings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_search" + android:icon="@drawable/ic_search" + android:title="@string/home_search" + app:showAsAction="always" /> + +</menu> diff --git a/src/android/app/src/main/res/navigation/settings_navigation.xml b/src/android/app/src/main/res/navigation/settings_navigation.xml index b36200c65..88e1b4587 100644 --- a/src/android/app/src/main/res/navigation/settings_navigation.xml +++ b/src/android/app/src/main/res/navigation/settings_navigation.xml @@ -15,10 +15,18 @@ android:name="game" app:argType="org.yuzu.yuzu_emu.model.Game" app:nullable="true" /> + <action + android:id="@+id/action_settingsFragment_to_settingsSearchFragment" + app:destination="@id/settingsSearchFragment" /> </fragment> <action android:id="@+id/action_global_settingsFragment" app:destination="@id/settingsFragment" /> + <fragment + android:id="@+id/settingsSearchFragment" + android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment" + android:label="SettingsSearchFragment" /> + </navigation> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 6b782780a..d43891cec 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -43,6 +43,7 @@ <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string> <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string> <string name="home_search_games">Search games</string> + <string name="search_settings">Search settings</string> <string name="games_dir_selected">Games directory selected</string> <string name="install_prod_keys">Install prod.keys</string> <string name="install_prod_keys_description">Required to decrypt retail games</string> |