diff options
Diffstat (limited to 'src/android')
22 files changed, 476 insertions, 243 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt index 7006651d0..bc6ff1364 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt @@ -49,6 +49,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List val context = YuzuApplication.appContext binding.textSettingName.text = context.getString(license.titleId) binding.textSettingDescription.text = context.getString(license.descriptionId) + binding.textSettingValue.visibility = View.GONE } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt index 481ddd5a5..6b46d359e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt @@ -5,13 +5,19 @@ package org.yuzu.yuzu_emu.adapters import android.text.Html import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import org.yuzu.yuzu_emu.databinding.PageSetupBinding +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.SetupCallback import org.yuzu.yuzu_emu.model.SetupPage +import org.yuzu.yuzu_emu.model.StepState +import org.yuzu.yuzu_emu.utils.ViewUtils class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) : RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() { @@ -26,7 +32,7 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) holder.bind(pages[position]) inner class SetupPageViewHolder(val binding: PageSetupBinding) : - RecyclerView.ViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root), SetupCallback { lateinit var page: SetupPage init { @@ -35,6 +41,12 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) fun bind(page: SetupPage) { this.page = page + + if (page.stepCompleted.invoke() == StepState.COMPLETE) { + binding.buttonAction.visibility = View.INVISIBLE + binding.textConfirmation.visibility = View.VISIBLE + } + binding.icon.setImageDrawable( ResourcesCompat.getDrawable( activity.resources, @@ -62,9 +74,15 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) MaterialButton.ICON_GRAVITY_END } setOnClickListener { - page.buttonAction.invoke() + page.buttonAction.invoke(this@SetupPageViewHolder) } } } + + override fun onStepCompleted() { + ViewUtils.hideView(binding.buttonAction, 200) + ViewUtils.showView(binding.textConfirmation, 200) + ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true) + } } } 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 ce0b92c90..9711e2c51 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 @@ -207,8 +207,11 @@ class SettingsAdapter( val sliderBinding = DialogSliderBinding.inflate(inflater) textSliderValue = sliderBinding.textValue - textSliderValue!!.text = sliderProgress.toString() - sliderBinding.textUnits.text = item.units + textSliderValue!!.text = String.format( + context.getString(R.string.value_with_units), + sliderProgress.toString(), + item.units + ) sliderBinding.slider.apply { valueFrom = item.min.toFloat() @@ -216,7 +219,11 @@ class SettingsAdapter( value = sliderProgress.toFloat() addOnChangeListener { _: Slider, value: Float, _: Boolean -> sliderProgress = value.toInt() - textSliderValue!!.text = sliderProgress.toString() + textSliderValue!!.text = String.format( + context.getString(R.string.value_with_units), + sliderProgress.toString(), + item.units + ) } } @@ -225,10 +232,6 @@ class SettingsAdapter( .setView(sliderBinding.root) .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, defaultCancelListener) - .setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int -> - sliderBinding.slider.value = item.defaultValue!!.toFloat() - onClick(dialog, which) - } .show() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 7955532ee..79572fc06 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt @@ -25,12 +25,17 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.visibility = View.VISIBLE } else { - val epochTime = setting.value.toLong() - val instant = Instant.ofEpochMilli(epochTime * 1000) - val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) - val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) - binding.textSettingDescription.text = dateFormatter.format(zonedTime) + binding.textSettingDescription.visibility = View.GONE } + + binding.textSettingValue.visibility = View.VISIBLE + val epochTime = setting.value.toLong() + val instant = Instant.ofEpochMilli(epochTime * 1000) + val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) + val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) + binding.textSettingValue.text = dateFormatter.format(zonedTime) + + setStyle(setting.isEditable, binding) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt index 5dad5945f..83a2e94f1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt @@ -23,6 +23,9 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA } else { binding.textSettingDescription.visibility = View.GONE } + binding.textSettingValue.visibility = View.GONE + + setStyle(setting.isEditable, binding) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt index f56460893..0fd1d2eaa 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt @@ -5,6 +5,8 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder import android.view.View import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter @@ -33,4 +35,18 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings abstract override fun onClick(clicked: View) abstract override fun onLongClick(clicked: View): Boolean + + fun setStyle(isEditable: Boolean, binding: ListItemSettingBinding) { + val opacity = if (isEditable) 1.0f else 0.5f + binding.textSettingName.alpha = opacity + binding.textSettingDescription.alpha = opacity + binding.textSettingValue.alpha = opacity + } + + fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) { + binding.switchWidget.isEnabled = isEditable + val opacity = if (isEditable) 1.0f else 0.5f + binding.textSettingName.alpha = opacity + binding.textSettingDescription.alpha = opacity + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index e4e321bd3..b42d955aa 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -17,28 +17,33 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti override fun bind(item: SettingsItem) { setting = item binding.textSettingName.setText(item.nameId) - binding.textSettingDescription.visibility = View.VISIBLE if (item.descriptionId != 0) { binding.textSettingDescription.setText(item.descriptionId) - } else if (item is SingleChoiceSetting) { - val resMgr = binding.textSettingDescription.context.resources + binding.textSettingDescription.visibility = View.VISIBLE + } else { + binding.textSettingDescription.visibility = View.GONE + } + + binding.textSettingValue.visibility = View.VISIBLE + if (item is SingleChoiceSetting) { + val resMgr = binding.textSettingValue.context.resources val values = resMgr.getIntArray(item.valuesId) for (i in values.indices) { if (values[i] == item.selectedValue) { - binding.textSettingDescription.text = resMgr.getStringArray(item.choicesId)[i] - return + binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] + break } } } else if (item is StringSingleChoiceSetting) { for (i in item.values!!.indices) { if (item.values[i] == item.selectedValue) { - binding.textSettingDescription.text = item.choices[i] - return + binding.textSettingValue.text = item.choices[i] + break } } - } else { - binding.textSettingDescription.visibility = View.GONE } + + setStyle(setting.isEditable, binding) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt index cc3f39aa5..a23b5d109 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt @@ -4,6 +4,7 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder import android.view.View +import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting @@ -22,6 +23,14 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda } else { binding.textSettingDescription.visibility = View.GONE } + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = String.format( + binding.textSettingValue.context.getString(R.string.value_with_units), + setting.selectedValue, + setting.units + ) + + setStyle(setting.isEditable, binding) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt index c545b4174..1cf581a9d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt @@ -22,6 +22,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd } else { binding.textSettingDescription.visibility = View.GONE } + binding.textSettingValue.visibility = View.GONE } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index 54f531795..ef34bf5f4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -25,12 +25,12 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter binding.textSettingDescription.text = "" binding.textSettingDescription.visibility = View.GONE } - binding.switchWidget.isChecked = setting.isChecked binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked) } + binding.switchWidget.isChecked = setting.isChecked - binding.switchWidget.isEnabled = setting.isEditable + setStyle(setting.isEditable, binding) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index 6c4ddaf6b..d50c421a0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -19,6 +19,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController @@ -32,10 +33,13 @@ import org.yuzu.yuzu_emu.adapters.SetupAdapter import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.SetupCallback import org.yuzu.yuzu_emu.model.SetupPage +import org.yuzu.yuzu_emu.model.StepState import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.GameHelper +import org.yuzu.yuzu_emu.utils.ViewUtils class SetupFragment : Fragment() { private var _binding: FragmentSetupBinding? = null @@ -112,14 +116,22 @@ class SetupFragment : Fragment() { 0, false, R.string.give_permission, - { permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) }, + { + notificationCallback = it + permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + }, true, R.string.notification_warning, R.string.notification_warning_description, 0, { - NotificationManagerCompat.from(requireContext()) + if (NotificationManagerCompat.from(requireContext()) .areNotificationsEnabled() + ) { + StepState.COMPLETE + } else { + StepState.INCOMPLETE + } } ) ) @@ -133,12 +145,22 @@ class SetupFragment : Fragment() { R.drawable.ic_add, true, R.string.select_keys, - { mainActivity.getProdKey.launch(arrayOf("*/*")) }, + { + keyCallback = it + getProdKey.launch(arrayOf("*/*")) + }, true, R.string.install_prod_keys_warning, R.string.install_prod_keys_warning_description, R.string.install_prod_keys_warning_help, - { File(DirectoryInitialization.userDirectory + "/keys/prod.keys").exists() } + { + val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys") + if (file.exists()) { + StepState.COMPLETE + } else { + StepState.INCOMPLETE + } + } ) ) add( @@ -150,9 +172,8 @@ class SetupFragment : Fragment() { true, R.string.add_games, { - mainActivity.getGamesDirectory.launch( - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data - ) + gamesDirCallback = it + getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, true, R.string.add_games_warning, @@ -163,7 +184,11 @@ class SetupFragment : Fragment() { PreferenceManager.getDefaultSharedPreferences( YuzuApplication.appContext ) - preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty() + if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) { + StepState.COMPLETE + } else { + StepState.INCOMPLETE + } } ) ) @@ -181,6 +206,13 @@ class SetupFragment : Fragment() { ) } + homeViewModel.shouldPageForward.observe(viewLifecycleOwner) { + if (it) { + pageForward() + homeViewModel.setShouldPageForward(false) + } + } + binding.viewPager2.apply { adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) offscreenPageLimit = 2 @@ -194,15 +226,15 @@ class SetupFragment : Fragment() { super.onPageSelected(position) if (position == 1 && previousPosition == 0) { - showView(binding.buttonNext) - showView(binding.buttonBack) + ViewUtils.showView(binding.buttonNext) + ViewUtils.showView(binding.buttonBack) } else if (position == 0 && previousPosition == 1) { - hideView(binding.buttonBack) - hideView(binding.buttonNext) + ViewUtils.hideView(binding.buttonBack) + ViewUtils.hideView(binding.buttonNext) } else if (position == pages.size - 1 && previousPosition == pages.size - 2) { - hideView(binding.buttonNext) + ViewUtils.hideView(binding.buttonNext) } else if (position == pages.size - 2 && previousPosition == pages.size - 1) { - showView(binding.buttonNext) + ViewUtils.showView(binding.buttonNext) } previousPosition = position @@ -215,7 +247,8 @@ class SetupFragment : Fragment() { // Checks if the user has completed the task on the current page if (currentPage.hasWarning) { - if (currentPage.taskCompleted.invoke()) { + val stepState = currentPage.stepCompleted.invoke() + if (stepState != StepState.INCOMPLETE) { pageForward() return@setOnClickListener } @@ -264,9 +297,15 @@ class SetupFragment : Fragment() { _binding = null } + private lateinit var notificationCallback: SetupCallback + @RequiresApi(Build.VERSION_CODES.TIRAMISU) private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it) { + notificationCallback.onStepCompleted() + } + if (!it && !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) ) { @@ -277,6 +316,27 @@ class SetupFragment : Fragment() { } } + private lateinit var keyCallback: SetupCallback + + val getProdKey = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + if (mainActivity.processKey(result)) { + keyCallback.onStepCompleted() + } + } + } + + private lateinit var gamesDirCallback: SetupCallback + + val getGamesDirectory = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> + if (result != null) { + mainActivity.processGamesDir(result) + gamesDirCallback.onStepCompleted() + } + } + private fun finishSetup() { PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) @@ -284,33 +344,6 @@ class SetupFragment : Fragment() { mainActivity.finishSetup(binding.root.findNavController()) } - private fun showView(view: View) { - view.apply { - alpha = 0f - visibility = View.VISIBLE - isClickable = true - }.animate().apply { - duration = 300 - alpha(1f) - }.start() - } - - private fun hideView(view: View) { - if (view.visibility == View.INVISIBLE) { - return - } - - view.apply { - alpha = 1f - isClickable = false - }.animate().apply { - duration = 300 - alpha(0f) - }.withEndAction { - view.visibility = View.INVISIBLE - } - } - fun pageForward() { binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1 } @@ -326,15 +359,29 @@ class SetupFragment : Fragment() { private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener( binding.root - ) { view: View, windowInsets: WindowInsetsCompat -> + ) { _: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) - view.setPadding( - barInsets.left + cutoutInsets.left, - barInsets.top + cutoutInsets.top, - barInsets.right + cutoutInsets.right, - barInsets.bottom + cutoutInsets.bottom - ) + + val leftPadding = barInsets.left + cutoutInsets.left + val topPadding = barInsets.top + cutoutInsets.top + val rightPadding = barInsets.right + cutoutInsets.right + val bottomPadding = barInsets.bottom + cutoutInsets.bottom + + if (resources.getBoolean(R.bool.small_layout)) { + binding.viewPager2 + .updatePadding(left = leftPadding, top = topPadding, right = rightPadding) + binding.constraintButtons + .updatePadding(left = leftPadding, right = rightPadding, bottom = bottomPadding) + } else { + binding.viewPager2.updatePadding(top = topPadding, bottom = bottomPadding) + binding.constraintButtons + .updatePadding( + left = leftPadding, + right = rightPadding, + bottom = bottomPadding + ) + } windowInsets } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt index 263ee7144..e13d84c9c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt @@ -14,6 +14,9 @@ class HomeViewModel : ViewModel() { private val _statusBarShadeVisible = MutableLiveData(true) val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible + private val _shouldPageForward = MutableLiveData(false) + val shouldPageForward: LiveData<Boolean> get() = _shouldPageForward + var navigatedToSetup = false init { @@ -33,4 +36,8 @@ class HomeViewModel : ViewModel() { } _statusBarShadeVisible.value = visible } + + fun setShouldPageForward(pageForward: Boolean) { + _shouldPageForward.value = pageForward + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt index a0c878e1c..09a128ae6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt @@ -10,10 +10,20 @@ data class SetupPage( val buttonIconId: Int, val leftAlignedIcon: Boolean, val buttonTextId: Int, - val buttonAction: () -> Unit, + val buttonAction: (callback: SetupCallback) -> Unit, val hasWarning: Boolean, val warningTitleId: Int = 0, val warningDescriptionId: Int = 0, val warningHelpLinkId: Int = 0, - val taskCompleted: () -> Boolean = { true } + val stepCompleted: () -> StepState = { StepState.UNDEFINED } ) + +interface SetupCallback { + fun onStepCompleted() +} + +enum class StepState { + COMPLETE, + INCOMPLETE, + UNDEFINED +} 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 f7d7aed1e..f77d06262 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 @@ -266,73 +266,80 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val getGamesDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> - if (result == null) { - return@registerForActivityResult + if (result != null) { + processGamesDir(result) } + } - contentResolver.takePersistableUriPermission( - result, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) + fun processGamesDir(result: Uri) { + contentResolver.takePersistableUriPermission( + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) - // When a new directory is picked, we currently will reset the existing games - // database. This effectively means that only one game directory is supported. - PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() - .putString(GameHelper.KEY_GAME_PATH, result.toString()) - .apply() + // When a new directory is picked, we currently will reset the existing games + // database. This effectively means that only one game directory is supported. + PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() + .putString(GameHelper.KEY_GAME_PATH, result.toString()) + .apply() - Toast.makeText( - applicationContext, - R.string.games_dir_selected, - Toast.LENGTH_LONG - ).show() + Toast.makeText( + applicationContext, + R.string.games_dir_selected, + Toast.LENGTH_LONG + ).show() - gamesViewModel.reloadGames(true) - } + gamesViewModel.reloadGames(true) + } val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) { - return@registerForActivityResult + if (result != null) { + processKey(result) } + } - if (FileUtil.getExtension(result) != "keys") { - MessageDialogFragment.newInstance( - R.string.reading_keys_failure, - R.string.install_prod_keys_failure_extension_description - ).show(supportFragmentManager, MessageDialogFragment.TAG) - return@registerForActivityResult - } + fun processKey(result: Uri): Boolean { + if (FileUtil.getExtension(result) != "keys") { + MessageDialogFragment.newInstance( + R.string.reading_keys_failure, + R.string.install_prod_keys_failure_extension_description + ).show(supportFragmentManager, MessageDialogFragment.TAG) + return false + } - contentResolver.takePersistableUriPermission( + contentResolver.takePersistableUriPermission( + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + val dstPath = DirectoryInitialization.userDirectory + "/keys/" + if (FileUtil.copyUriToInternalStorage( + applicationContext, result, - Intent.FLAG_GRANT_READ_URI_PERMISSION + dstPath, + "prod.keys" ) - - val dstPath = DirectoryInitialization.userDirectory + "/keys/" - if (FileUtil.copyUriToInternalStorage( + ) { + if (NativeLibrary.reloadKeys()) { + Toast.makeText( applicationContext, - result, - dstPath, - "prod.keys" - ) - ) { - if (NativeLibrary.reloadKeys()) { - Toast.makeText( - applicationContext, - R.string.install_keys_success, - Toast.LENGTH_SHORT - ).show() - gamesViewModel.reloadGames(true) - } else { - MessageDialogFragment.newInstance( - R.string.invalid_keys_error, - R.string.install_keys_failure_description, - R.string.dumping_keys_quickstart_link - ).show(supportFragmentManager, MessageDialogFragment.TAG) - } + R.string.install_keys_success, + Toast.LENGTH_SHORT + ).show() + gamesViewModel.reloadGames(true) + return true + } else { + MessageDialogFragment.newInstance( + R.string.invalid_keys_error, + R.string.install_keys_failure_description, + R.string.dumping_keys_quickstart_link + ).show(supportFragmentManager, MessageDialogFragment.TAG) + return false } } + return false + } val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt new file mode 100644 index 000000000..f9a3e4126 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.view.View + +object ViewUtils { + fun showView(view: View, length: Long = 300) { + view.apply { + alpha = 0f + visibility = View.VISIBLE + isClickable = true + }.animate().apply { + duration = length + alpha(1f) + }.start() + } + + fun hideView(view: View, length: Long = 300) { + if (view.visibility == View.INVISIBLE) { + return + } + + view.apply { + alpha = 1f + isClickable = false + }.animate().apply { + duration = length + alpha(0f) + }.withEndAction { + view.visibility = View.INVISIBLE + }.start() + } +} diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml index cbe631d88..406df9eab 100644 --- a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml +++ b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/setup_root" @@ -8,33 +8,39 @@ <androidx.viewpager2.widget.ViewPager2 android:id="@+id/viewPager2" - 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_toTopOf="parent" /> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentTop="true" + android:layout_alignParentBottom="true" + android:clipToPadding="false" /> - <com.google.android.material.button.MaterialButton - style="@style/Widget.Material3.Button.TextButton" - android:id="@+id/button_next" - android:layout_width="wrap_content" + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/constraint_buttons" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="16dp" - android:text="@string/next" - android:visibility="invisible" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> + android:layout_alignParentBottom="true" + android:layout_margin="8dp"> - <com.google.android.material.button.MaterialButton - android:id="@+id/button_back" - style="@style/Widget.Material3.Button.TextButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:text="@string/back" - android:visibility="invisible" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" /> + <com.google.android.material.button.MaterialButton + android:id="@+id/button_next" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/next" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button_back" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/back" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> -</androidx.constraintlayout.widget.ConstraintLayout> +</RelativeLayout> diff --git a/src/android/app/src/main/res/layout-w600dp/page_setup.xml b/src/android/app/src/main/res/layout-w600dp/page_setup.xml index e1c26b2f8..9e0ab8ecb 100644 --- a/src/android/app/src/main/res/layout-w600dp/page_setup.xml +++ b/src/android/app/src/main/res/layout-w600dp/page_setup.xml @@ -21,45 +21,76 @@ </LinearLayout> - <LinearLayout + <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_weight="1" - android:orientation="vertical" - android:gravity="center"> + android:layout_weight="1"> <com.google.android.material.textview.MaterialTextView - style="@style/TextAppearance.Material3.DisplaySmall" android:id="@+id/text_title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textAlignment="center" + style="@style/TextAppearance.Material3.DisplaySmall" + android:layout_width="0dp" + android:layout_height="0dp" + android:gravity="center" android:textColor="?attr/colorOnSurface" android:textStyle="bold" + app:layout_constraintBottom_toTopOf="@+id/text_description" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_weight="2" tools:text="@string/welcome" /> <com.google.android.material.textview.MaterialTextView - style="@style/TextAppearance.Material3.TitleLarge" android:id="@+id/text_description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:paddingHorizontal="32dp" - android:textAlignment="center" - android:textSize="26sp" - app:lineHeight="40sp" + style="@style/TextAppearance.Material3.TitleLarge" + android:layout_width="0dp" + android:layout_height="0dp" + android:gravity="center" + android:textSize="20sp" + android:paddingHorizontal="16dp" + app:layout_constraintBottom_toTopOf="@+id/button_action" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/text_title" + app:layout_constraintVertical_weight="2" + app:lineHeight="30sp" tools:text="@string/welcome_description" /> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_confirmation" + style="@style/TextAppearance.Material3.TitleLarge" + android:layout_width="0dp" + android:layout_height="0dp" + android:paddingHorizontal="16dp" + android:paddingBottom="20dp" + android:gravity="center" + android:textSize="30sp" + android:visibility="invisible" + android:text="@string/step_complete" + android:textStyle="bold" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/text_description" + app:layout_constraintVertical_weight="1" + app:lineHeight="30sp" /> + <com.google.android.material.button.MaterialButton android:id="@+id/button_action" android:layout_width="wrap_content" android:layout_height="56dp" - android:layout_marginTop="32dp" + android:layout_marginTop="16dp" + android:layout_marginBottom="48dp" android:textSize="20sp" - app:iconSize="24sp" app:iconGravity="end" + app:iconSize="24sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/text_description" tools:text="Get started" /> - </LinearLayout> + </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> diff --git a/src/android/app/src/main/res/layout/dialog_slider.xml b/src/android/app/src/main/res/layout/dialog_slider.xml index 8c84cb606..d1cb31739 100644 --- a/src/android/app/src/main/res/layout/dialog_slider.xml +++ b/src/android/app/src/main/res/layout/dialog_slider.xml @@ -5,23 +5,16 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <TextView + <com.google.android.material.textview.MaterialTextView android:id="@+id/text_value" + style="@style/TextAppearance.Material3.LabelMedium" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:layout_marginBottom="@dimen/spacing_medlarge" android:layout_marginTop="@dimen/spacing_medlarge" - tools:text="75" /> - - <TextView - android:id="@+id/text_units" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignTop="@+id/text_value" - android:layout_toEndOf="@+id/text_value" - tools:text="%" /> + tools:text="75%" /> <com.google.android.material.slider.Slider android:id="@+id/slider" diff --git a/src/android/app/src/main/res/layout/fragment_setup.xml b/src/android/app/src/main/res/layout/fragment_setup.xml index d7bafaea2..9499f6463 100644 --- a/src/android/app/src/main/res/layout/fragment_setup.xml +++ b/src/android/app/src/main/res/layout/fragment_setup.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/setup_root" @@ -8,35 +8,39 @@ <androidx.viewpager2.widget.ViewPager2 android:id="@+id/viewPager2" - android:layout_width="0dp" - android:layout_height="0dp" - android:clipToPadding="false" - android:layout_marginBottom="16dp" - app:layout_constraintBottom_toTopOf="@+id/button_next" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <com.google.android.material.button.MaterialButton - style="@style/Widget.Material3.Button.TextButton" - android:id="@+id/button_next" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="12dp" - android:text="@string/next" - android:visibility="invisible" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> + android:layout_above="@+id/constraint_buttons" + android:layout_alignParentTop="true" + android:clipToPadding="false" /> - <com.google.android.material.button.MaterialButton - style="@style/Widget.Material3.Button.TextButton" - android:id="@+id/button_back" - android:layout_width="wrap_content" + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/constraint_buttons" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="12dp" - android:text="@string/back" - android:visibility="invisible" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" /> + android:layout_margin="8dp" + android:layout_alignParentBottom="true"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button_next" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/next" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button_back" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/back" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> -</androidx.constraintlayout.widget.ConstraintLayout> +</RelativeLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml index ec896342b..f1037a740 100644 --- a/src/android/app/src/main/res/layout/list_item_setting.xml +++ b/src/android/app/src/main/res/layout/list_item_setting.xml @@ -1,9 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +<RelativeLayout + 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="wrap_content" - xmlns:app="http://schemas.android.com/apk/res-auto" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" @@ -11,31 +12,40 @@ android:minHeight="72dp" android:padding="@dimen/spacing_large"> - <com.google.android.material.textview.MaterialTextView - style="@style/TextAppearance.Material3.HeadlineMedium" - android:id="@+id/text_setting_name" - android:layout_width="0dp" + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentEnd="true" - android:layout_alignParentStart="true" - android:layout_alignParentTop="true" - android:textSize="16sp" - android:textAlignment="viewStart" - app:lineHeight="28dp" - tools:text="Setting Name" /> + android:orientation="vertical"> - <TextView - style="@style/TextAppearance.Material3.BodySmall" - android:id="@+id/text_setting_description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentEnd="true" - android:layout_alignParentStart="true" - android:layout_alignStart="@+id/text_setting_name" - android:layout_below="@+id/text_setting_name" - android:layout_marginTop="@dimen/spacing_small" - android:visibility="visible" - android:textAlignment="viewStart" - tools:text="@string/app_disclaimer" /> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.Material3.HeadlineMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textSize="16sp" + app:lineHeight="22dp" + tools:text="Setting Name" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_description" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:textAlignment="viewStart" + tools:text="@string/app_disclaimer" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_value" + style="@style/TextAppearance.Material3.LabelMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:textAlignment="viewStart" + android:textStyle="bold" + tools:text="1x" /> + + </LinearLayout> </RelativeLayout> diff --git a/src/android/app/src/main/res/layout/page_setup.xml b/src/android/app/src/main/res/layout/page_setup.xml index 1436ef308..535abcf02 100644 --- a/src/android/app/src/main/res/layout/page_setup.xml +++ b/src/android/app/src/main/res/layout/page_setup.xml @@ -21,11 +21,12 @@ app:layout_constraintVertical_chainStyle="spread" app:layout_constraintWidth_max="220dp" app:layout_constraintWidth_min="110dp" - app:layout_constraintVertical_weight="3" /> + app:layout_constraintVertical_weight="3" + tools:src="@drawable/ic_notification" /> <com.google.android.material.textview.MaterialTextView android:id="@+id/text_title" - style="@style/TextAppearance.Material3.DisplayMedium" + style="@style/TextAppearance.Material3.DisplaySmall" android:layout_width="0dp" android:layout_height="0dp" android:textAlignment="center" @@ -44,23 +45,42 @@ android:layout_width="0dp" android:layout_height="0dp" android:textAlignment="center" - android:textSize="26sp" + android:textSize="20sp" android:paddingHorizontal="16dp" app:layout_constraintBottom_toTopOf="@+id/button_action" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/text_title" app:layout_constraintVertical_weight="2" - app:lineHeight="40sp" + app:lineHeight="30sp" tools:text="@string/welcome_description" /> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_confirmation" + style="@style/TextAppearance.Material3.TitleLarge" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:paddingHorizontal="16dp" + android:paddingTop="24dp" + android:textAlignment="center" + android:textSize="30sp" + android:visibility="invisible" + android:text="@string/step_complete" + android:textStyle="bold" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/text_description" + app:layout_constraintVertical_weight="1" + app:lineHeight="30sp" /> + <com.google.android.material.button.MaterialButton android:id="@+id/button_action" android:layout_width="wrap_content" android:layout_height="56dp" - android:textSize="20sp" android:layout_marginTop="16dp" android:layout_marginBottom="48dp" + android:textSize="20sp" app:iconGravity="end" app:iconSize="24sp" app:layout_constraintBottom_toBottomOf="parent" diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 02e25504d..de1b2909b 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ <string name="back">Back</string> <string name="add_games">Add Games</string> <string name="add_games_description">Select your games folder</string> + <string name="step_complete">Complete!</string> <!-- Home strings --> <string name="home_games">Games</string> @@ -149,6 +150,7 @@ <string name="frame_limit_slider">Limit speed percent</string> <string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string> <string name="cpu_accuracy">CPU accuracy</string> + <string name="value_with_units">%1$s%2$s</string> <!-- System settings strings --> <string name="use_docked_mode">Docked Mode</string> |