8 changed files with 372 additions and 1 deletions
-
3src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
-
34src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
-
189src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt
-
7src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
-
120src/android/app/src/main/res/layout/fragment_settings_search.xml
-
11src/android/app/src/main/res/menu/menu_settings.xml
-
8src/android/app/src/main/res/navigation/settings_navigation.xml
-
1src/android/app/src/main/res/values/strings.xml
@ -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" |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue