summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar liamwhite2024-01-12 10:01:54 -0500
committerGravatar GitHub2024-01-12 10:01:54 -0500
commitfcb0dff67ce18bde935fa1d79f653ae418dced8d (patch)
tree338799be4b3eb1ea714fee96fa7c6333a077819f /src
parentMerge pull request #12653 from liamwhite/once-more (diff)
parentandroid: Fix added driver path (diff)
downloadyuzu-fcb0dff67ce18bde935fa1d79f653ae418dced8d.tar.gz
yuzu-fcb0dff67ce18bde935fa1d79f653ae418dced8d.tar.xz
yuzu-fcb0dff67ce18bde935fa1d79f653ae418dced8d.zip
Merge pull request #12642 from t895/adapter-refactor
android: Refactor list adapters
Diffstat (limited to 'src')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt33
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractListAdapter.kt98
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractSingleSelectionList.kt105
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt36
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt88
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt57
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt101
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt36
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt192
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt28
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt72
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt36
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt50
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt45
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt76
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Driver.kt27
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt129
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SelectableItem.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholder/AbstractViewHolder.kt18
-rw-r--r--src/android/app/src/main/res/menu/menu_driver_manager.xml11
22 files changed, 665 insertions, 588 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt
new file mode 100644
index 000000000..f006f9e3d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt
@@ -0,0 +1,33 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.annotation.SuppressLint
7import androidx.recyclerview.widget.AsyncDifferConfig
8import androidx.recyclerview.widget.DiffUtil
9import androidx.recyclerview.widget.ListAdapter
10import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
11import androidx.recyclerview.widget.RecyclerView
12
13/**
14 * Generic adapter that implements an [AsyncDifferConfig] and covers some of the basic boilerplate
15 * code used in every [RecyclerView].
16 * Type assigned to [Model] must inherit from [Object] in order to be compared properly.
17 */
18abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>> :
19 ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>()).build()) {
20 override fun onBindViewHolder(holder: Holder, position: Int) =
21 holder.bind(currentList[position])
22
23 private class DiffCallback<Model> : DiffUtil.ItemCallback<Model>() {
24 override fun areItemsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
25 return oldItem === newItem
26 }
27
28 @SuppressLint("DiffUtilEquals")
29 override fun areContentsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
30 return oldItem == newItem
31 }
32 }
33}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractListAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractListAdapter.kt
new file mode 100644
index 000000000..3dfee3d0c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractListAdapter.kt
@@ -0,0 +1,98 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.annotation.SuppressLint
7import androidx.recyclerview.widget.RecyclerView
8import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
9
10/**
11 * Generic list class meant to take care of basic lists
12 * @param currentList The list to show initially
13 */
14abstract class AbstractListAdapter<Model : Any, Holder : AbstractViewHolder<Model>>(
15 open var currentList: List<Model>
16) : RecyclerView.Adapter<Holder>() {
17 override fun onBindViewHolder(holder: Holder, position: Int) =
18 holder.bind(currentList[position])
19
20 override fun getItemCount(): Int = currentList.size
21
22 /**
23 * Adds an item to [currentList] and notifies the underlying adapter of the change. If no parameter
24 * is passed in for position, [item] is added to the end of the list. Invokes [callback] last.
25 * @param item The item to add to the list
26 * @param position Index where [item] will be added
27 * @param callback Lambda that's called at the end of the list changes and has the added list
28 * position passed in as a parameter
29 */
30 open fun addItem(item: Model, position: Int = -1, callback: ((position: Int) -> Unit)? = null) {
31 val newList = currentList.toMutableList()
32 val positionToUpdate: Int
33 if (position == -1) {
34 newList.add(item)
35 currentList = newList
36 positionToUpdate = currentList.size - 1
37 } else {
38 newList.add(position, item)
39 currentList = newList
40 positionToUpdate = position
41 }
42 onItemAdded(positionToUpdate, callback)
43 }
44
45 protected fun onItemAdded(position: Int, callback: ((Int) -> Unit)? = null) {
46 notifyItemInserted(position)
47 callback?.invoke(position)
48 }
49
50 /**
51 * Replaces the [item] at [position] in the [currentList] and notifies the underlying adapter
52 * of the change. Invokes [callback] last.
53 * @param item New list item
54 * @param position Index where [item] will replace the existing list item
55 * @param callback Lambda that's called at the end of the list changes and has the changed list
56 * position passed in as a parameter
57 */
58 fun changeItem(item: Model, position: Int, callback: ((position: Int) -> Unit)? = null) {
59 val newList = currentList.toMutableList()
60 newList[position] = item
61 currentList = newList
62 onItemChanged(position, callback)
63 }
64
65 protected fun onItemChanged(position: Int, callback: ((Int) -> Unit)? = null) {
66 notifyItemChanged(position)
67 callback?.invoke(position)
68 }
69
70 /**
71 * Removes the list item at [position] in [currentList] and notifies the underlying adapter
72 * of the change. Invokes [callback] last.
73 * @param position Index where the list item will be removed
74 * @param callback Lambda that's called at the end of the list changes and has the removed list
75 * position passed in as a parameter
76 */
77 fun removeItem(position: Int, callback: ((position: Int) -> Unit)? = null) {
78 val newList = currentList.toMutableList()
79 newList.removeAt(position)
80 currentList = newList
81 onItemRemoved(position, callback)
82 }
83
84 protected fun onItemRemoved(position: Int, callback: ((Int) -> Unit)? = null) {
85 notifyItemRemoved(position)
86 callback?.invoke(position)
87 }
88
89 /**
90 * Replaces [currentList] with [newList] and notifies the underlying adapter of the change.
91 * @param newList The new list to replace [currentList]
92 */
93 @SuppressLint("NotifyDataSetChanged")
94 open fun replaceList(newList: List<Model>) {
95 currentList = newList
96 notifyDataSetChanged()
97 }
98}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractSingleSelectionList.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractSingleSelectionList.kt
new file mode 100644
index 000000000..52163f9d7
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractSingleSelectionList.kt
@@ -0,0 +1,105 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import org.yuzu.yuzu_emu.model.SelectableItem
7import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
8
9/**
10 * Generic list class meant to take care of single selection UI updates
11 * @param currentList The list to show initially
12 * @param defaultSelection The default selection to use if no list items are selected by
13 * [SelectableItem.selected] or if the currently selected item is removed from the list
14 */
15abstract class AbstractSingleSelectionList<
16 Model : SelectableItem,
17 Holder : AbstractViewHolder<Model>
18 >(
19 final override var currentList: List<Model>,
20 private val defaultSelection: DefaultSelection = DefaultSelection.Start
21) : AbstractListAdapter<Model, Holder>(currentList) {
22 var selectedItem = getDefaultSelection()
23
24 init {
25 findSelectedItem()
26 }
27
28 /**
29 * Changes the selection state of the [SelectableItem] that was selected and the previously selected
30 * item and notifies the underlying adapter of the change for those items. Invokes [callback] last.
31 * Does nothing if [position] is the same as the currently selected item.
32 * @param position Index of the item that was selected
33 * @param callback Lambda that's called at the end of the list changes and has the selected list
34 * position passed in as a parameter
35 */
36 fun selectItem(position: Int, callback: ((position: Int) -> Unit)? = null) {
37 if (position == selectedItem) {
38 return
39 }
40
41 val previouslySelectedItem = selectedItem
42 selectedItem = position
43 if (currentList.indices.contains(selectedItem)) {
44 currentList[selectedItem].onSelectionStateChanged(true)
45 }
46 if (currentList.indices.contains(previouslySelectedItem)) {
47 currentList[previouslySelectedItem].onSelectionStateChanged(false)
48 }
49 onItemChanged(previouslySelectedItem)
50 onItemChanged(selectedItem)
51 callback?.invoke(position)
52 }
53
54 /**
55 * Removes a given item from the list and notifies the underlying adapter of the change. If the
56 * currently selected item was the item that was removed, the item at the position provided
57 * by [defaultSelection] will be made the new selection. Invokes [callback] last.
58 * @param position Index of the item that was removed
59 * @param callback Lambda that's called at the end of the list changes and has the removed and
60 * selected list positions passed in as parameters
61 */
62 fun removeSelectableItem(
63 position: Int,
64 callback: ((removedPosition: Int, selectedPosition: Int) -> Unit)?
65 ) {
66 removeItem(position)
67 if (position == selectedItem) {
68 selectedItem = getDefaultSelection()
69 currentList[selectedItem].onSelectionStateChanged(true)
70 onItemChanged(selectedItem)
71 } else if (position < selectedItem) {
72 selectedItem--
73 }
74 callback?.invoke(position, selectedItem)
75 }
76
77 override fun addItem(item: Model, position: Int, callback: ((Int) -> Unit)?) {
78 super.addItem(item, position, callback)
79 if (position <= selectedItem && position != -1) {
80 selectedItem++
81 }
82 }
83
84 override fun replaceList(newList: List<Model>) {
85 super.replaceList(newList)
86 findSelectedItem()
87 }
88
89 private fun findSelectedItem() {
90 for (i in currentList.indices) {
91 if (currentList[i].selected) {
92 selectedItem = i
93 break
94 }
95 }
96 }
97
98 private fun getDefaultSelection(): Int =
99 when (defaultSelection) {
100 DefaultSelection.Start -> currentList.indices.first
101 DefaultSelection.End -> currentList.indices.last
102 }
103
104 enum class DefaultSelection { Start, End }
105}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
index 15c7ca3c9..94c151325 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
@@ -5,48 +5,28 @@ package org.yuzu.yuzu_emu.adapters
5 5
6import android.view.LayoutInflater 6import android.view.LayoutInflater
7import android.view.ViewGroup 7import android.view.ViewGroup
8import androidx.recyclerview.widget.AsyncDifferConfig
9import androidx.recyclerview.widget.DiffUtil
10import androidx.recyclerview.widget.ListAdapter
11import androidx.recyclerview.widget.RecyclerView
12import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding 8import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
13import org.yuzu.yuzu_emu.model.Addon 9import org.yuzu.yuzu_emu.model.Addon
10import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
14 11
15class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>( 12class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() {
16 AsyncDifferConfig.Builder(DiffCallback()).build()
17) {
18 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { 13 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
19 ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) 14 ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
20 .also { return AddonViewHolder(it) } 15 .also { return AddonViewHolder(it) }
21 } 16 }
22 17
23 override fun getItemCount(): Int = currentList.size
24
25 override fun onBindViewHolder(holder: AddonViewHolder, position: Int) =
26 holder.bind(currentList[position])
27
28 inner class AddonViewHolder(val binding: ListItemAddonBinding) : 18 inner class AddonViewHolder(val binding: ListItemAddonBinding) :
29 RecyclerView.ViewHolder(binding.root) { 19 AbstractViewHolder<Addon>(binding) {
30 fun bind(addon: Addon) { 20 override fun bind(model: Addon) {
31 binding.root.setOnClickListener { 21 binding.root.setOnClickListener {
32 binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked 22 binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
33 } 23 }
34 binding.title.text = addon.title 24 binding.title.text = model.title
35 binding.version.text = addon.version 25 binding.version.text = model.version
36 binding.addonSwitch.setOnCheckedChangeListener { _, checked -> 26 binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
37 addon.enabled = checked 27 model.enabled = checked
38 } 28 }
39 binding.addonSwitch.isChecked = addon.enabled 29 binding.addonSwitch.isChecked = model.enabled
40 }
41 }
42
43 private class DiffCallback : DiffUtil.ItemCallback<Addon>() {
44 override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean {
45 return oldItem == newItem
46 }
47
48 override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean {
49 return oldItem == newItem
50 } 30 }
51 } 31 }
52} 32}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
index 4a05c5be9..41d7f72b8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
@@ -4,13 +4,11 @@
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.view.LayoutInflater 6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup 7import android.view.ViewGroup
9import android.widget.Toast 8import android.widget.Toast
10import androidx.core.content.res.ResourcesCompat 9import androidx.core.content.res.ResourcesCompat
11import androidx.fragment.app.FragmentActivity 10import androidx.fragment.app.FragmentActivity
12import androidx.navigation.findNavController 11import androidx.navigation.findNavController
13import androidx.recyclerview.widget.RecyclerView
14import org.yuzu.yuzu_emu.HomeNavigationDirections 12import org.yuzu.yuzu_emu.HomeNavigationDirections
15import org.yuzu.yuzu_emu.NativeLibrary 13import org.yuzu.yuzu_emu.NativeLibrary
16import org.yuzu.yuzu_emu.R 14import org.yuzu.yuzu_emu.R
@@ -19,72 +17,58 @@ import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
19import org.yuzu.yuzu_emu.model.Applet 17import org.yuzu.yuzu_emu.model.Applet
20import org.yuzu.yuzu_emu.model.AppletInfo 18import org.yuzu.yuzu_emu.model.AppletInfo
21import org.yuzu.yuzu_emu.model.Game 19import org.yuzu.yuzu_emu.model.Game
20import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
22 21
23class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) : 22class AppletAdapter(val activity: FragmentActivity, applets: List<Applet>) :
24 RecyclerView.Adapter<AppletAdapter.AppletViewHolder>(), 23 AbstractListAdapter<Applet, AppletAdapter.AppletViewHolder>(applets) {
25 View.OnClickListener {
26
27 override fun onCreateViewHolder( 24 override fun onCreateViewHolder(
28 parent: ViewGroup, 25 parent: ViewGroup,
29 viewType: Int 26 viewType: Int
30 ): AppletAdapter.AppletViewHolder { 27 ): AppletAdapter.AppletViewHolder {
31 CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false) 28 CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
32 .apply { root.setOnClickListener(this@AppletAdapter) }
33 .also { return AppletViewHolder(it) } 29 .also { return AppletViewHolder(it) }
34 } 30 }
35 31
36 override fun onBindViewHolder(holder: AppletViewHolder, position: Int) =
37 holder.bind(applets[position])
38
39 override fun getItemCount(): Int = applets.size
40
41 override fun onClick(view: View) {
42 val applet = (view.tag as AppletViewHolder).applet
43 val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
44 if (appletPath.isEmpty()) {
45 Toast.makeText(
46 YuzuApplication.appContext,
47 R.string.applets_error_applet,
48 Toast.LENGTH_SHORT
49 ).show()
50 return
51 }
52
53 if (applet.appletInfo == AppletInfo.Cabinet) {
54 view.findNavController()
55 .navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
56 return
57 }
58
59 NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
60 val appletGame = Game(
61 title = YuzuApplication.appContext.getString(applet.titleId),
62 path = appletPath
63 )
64 val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
65 view.findNavController().navigate(action)
66 }
67
68 inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) : 32 inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
69 RecyclerView.ViewHolder(binding.root) { 33 AbstractViewHolder<Applet>(binding) {
70 lateinit var applet: Applet 34 override fun bind(model: Applet) {
71 35 binding.title.setText(model.titleId)
72 init { 36 binding.description.setText(model.descriptionId)
73 itemView.tag = this
74 }
75
76 fun bind(applet: Applet) {
77 this.applet = applet
78
79 binding.title.setText(applet.titleId)
80 binding.description.setText(applet.descriptionId)
81 binding.icon.setImageDrawable( 37 binding.icon.setImageDrawable(
82 ResourcesCompat.getDrawable( 38 ResourcesCompat.getDrawable(
83 binding.icon.context.resources, 39 binding.icon.context.resources,
84 applet.iconId, 40 model.iconId,
85 binding.icon.context.theme 41 binding.icon.context.theme
86 ) 42 )
87 ) 43 )
44
45 binding.root.setOnClickListener { onClick(model) }
46 }
47
48 fun onClick(applet: Applet) {
49 val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
50 if (appletPath.isEmpty()) {
51 Toast.makeText(
52 binding.root.context,
53 R.string.applets_error_applet,
54 Toast.LENGTH_SHORT
55 ).show()
56 return
57 }
58
59 if (applet.appletInfo == AppletInfo.Cabinet) {
60 binding.root.findNavController()
61 .navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
62 return
63 }
64
65 NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
66 val appletGame = Game(
67 title = YuzuApplication.appContext.getString(applet.titleId),
68 path = appletPath
69 )
70 val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
71 binding.root.findNavController().navigate(action)
88 } 72 }
89 } 73 }
90} 74}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt
index e7b7c0f2f..a56137148 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt
@@ -4,12 +4,10 @@
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.view.LayoutInflater 6import android.view.LayoutInflater
7import android.view.View
8import android.view.ViewGroup 7import android.view.ViewGroup
9import androidx.core.content.res.ResourcesCompat 8import androidx.core.content.res.ResourcesCompat
10import androidx.fragment.app.Fragment 9import androidx.fragment.app.Fragment
11import androidx.navigation.fragment.findNavController 10import androidx.navigation.fragment.findNavController
12import androidx.recyclerview.widget.RecyclerView
13import org.yuzu.yuzu_emu.HomeNavigationDirections 11import org.yuzu.yuzu_emu.HomeNavigationDirections
14import org.yuzu.yuzu_emu.NativeLibrary 12import org.yuzu.yuzu_emu.NativeLibrary
15import org.yuzu.yuzu_emu.R 13import org.yuzu.yuzu_emu.R
@@ -19,54 +17,43 @@ import org.yuzu.yuzu_emu.model.CabinetMode
19import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder 17import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder
20import org.yuzu.yuzu_emu.model.AppletInfo 18import org.yuzu.yuzu_emu.model.AppletInfo
21import org.yuzu.yuzu_emu.model.Game 19import org.yuzu.yuzu_emu.model.Game
20import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
22 21
23class CabinetLauncherDialogAdapter(val fragment: Fragment) : 22class CabinetLauncherDialogAdapter(val fragment: Fragment) :
24 RecyclerView.Adapter<CabinetModeViewHolder>(), 23 AbstractListAdapter<CabinetMode, CabinetModeViewHolder>(
25 View.OnClickListener { 24 CabinetMode.values().copyOfRange(1, CabinetMode.entries.size).toList()
26 private val cabinetModes = CabinetMode.values().copyOfRange(1, CabinetMode.values().size) 25 ) {
27 26
28 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder { 27 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder {
29 DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) 28 DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
30 .apply { root.setOnClickListener(this@CabinetLauncherDialogAdapter) }
31 .also { return CabinetModeViewHolder(it) } 29 .also { return CabinetModeViewHolder(it) }
32 } 30 }
33 31
34 override fun getItemCount(): Int = cabinetModes.size
35
36 override fun onBindViewHolder(holder: CabinetModeViewHolder, position: Int) =
37 holder.bind(cabinetModes[position])
38
39 override fun onClick(view: View) {
40 val mode = (view.tag as CabinetModeViewHolder).cabinetMode
41 val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId)
42 NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId)
43 NativeLibrary.setCabinetMode(mode.id)
44 val appletGame = Game(
45 title = YuzuApplication.appContext.getString(R.string.cabinet_applet),
46 path = appletPath
47 )
48 val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
49 fragment.findNavController().navigate(action)
50 }
51
52 inner class CabinetModeViewHolder(val binding: DialogListItemBinding) : 32 inner class CabinetModeViewHolder(val binding: DialogListItemBinding) :
53 RecyclerView.ViewHolder(binding.root) { 33 AbstractViewHolder<CabinetMode>(binding) {
54 lateinit var cabinetMode: CabinetMode 34 override fun bind(model: CabinetMode) {
55
56 init {
57 itemView.tag = this
58 }
59
60 fun bind(cabinetMode: CabinetMode) {
61 this.cabinetMode = cabinetMode
62 binding.icon.setImageDrawable( 35 binding.icon.setImageDrawable(
63 ResourcesCompat.getDrawable( 36 ResourcesCompat.getDrawable(
64 binding.icon.context.resources, 37 binding.icon.context.resources,
65 cabinetMode.iconId, 38 model.iconId,
66 binding.icon.context.theme 39 binding.icon.context.theme
67 ) 40 )
68 ) 41 )
69 binding.title.setText(cabinetMode.titleId) 42 binding.title.setText(model.titleId)
43
44 binding.root.setOnClickListener { onClick(model) }
45 }
46
47 private fun onClick(mode: CabinetMode) {
48 val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId)
49 NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId)
50 NativeLibrary.setCabinetMode(mode.id)
51 val appletGame = Game(
52 title = YuzuApplication.appContext.getString(R.string.cabinet_applet),
53 path = appletPath
54 )
55 val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
56 fragment.findNavController().navigate(action)
70 } 57 }
71 } 58 }
72} 59}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
index d290a656c..d6f17cf29 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
@@ -7,65 +7,39 @@ import android.text.TextUtils
7import android.view.LayoutInflater 7import android.view.LayoutInflater
8import android.view.View 8import android.view.View
9import android.view.ViewGroup 9import android.view.ViewGroup
10import androidx.recyclerview.widget.AsyncDifferConfig
11import androidx.recyclerview.widget.DiffUtil
12import androidx.recyclerview.widget.ListAdapter
13import androidx.recyclerview.widget.RecyclerView
14import org.yuzu.yuzu_emu.R
15import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding 10import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
11import org.yuzu.yuzu_emu.features.settings.model.StringSetting
12import org.yuzu.yuzu_emu.model.Driver
16import org.yuzu.yuzu_emu.model.DriverViewModel 13import org.yuzu.yuzu_emu.model.DriverViewModel
17import org.yuzu.yuzu_emu.utils.GpuDriverHelper 14import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
18import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
19 15
20class DriverAdapter(private val driverViewModel: DriverViewModel) : 16class DriverAdapter(private val driverViewModel: DriverViewModel) :
21 ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>( 17 AbstractSingleSelectionList<Driver, DriverAdapter.DriverViewHolder>(
22 AsyncDifferConfig.Builder(DiffCallback()).build() 18 driverViewModel.driverList.value
23 ) { 19 ) {
24 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder { 20 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
25 val binding = 21 CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
26 CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) 22 .also { return DriverViewHolder(it) }
27 return DriverViewHolder(binding)
28 }
29
30 override fun getItemCount(): Int = currentList.size
31
32 override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
33 holder.bind(currentList[position])
34
35 private fun onSelectDriver(position: Int) {
36 driverViewModel.setSelectedDriverIndex(position)
37 notifyItemChanged(driverViewModel.previouslySelectedDriver)
38 notifyItemChanged(driverViewModel.selectedDriver)
39 }
40
41 private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) {
42 if (driverViewModel.selectedDriver > position) {
43 driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
44 }
45 if (GpuDriverHelper.customDriverSettingData == driverData.second) {
46 driverViewModel.setSelectedDriverIndex(0)
47 }
48 driverViewModel.driversToDelete.add(driverData.first)
49 driverViewModel.removeDriver(driverData)
50 notifyItemRemoved(position)
51 notifyItemChanged(driverViewModel.selectedDriver)
52 } 23 }
53 24
54 inner class DriverViewHolder(val binding: CardDriverOptionBinding) : 25 inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
55 RecyclerView.ViewHolder(binding.root) { 26 AbstractViewHolder<Driver>(binding) {
56 private lateinit var driverData: Pair<String, GpuDriverMetadata> 27 override fun bind(model: Driver) {
57
58 fun bind(driverData: Pair<String, GpuDriverMetadata>) {
59 this.driverData = driverData
60 val driver = driverData.second
61
62 binding.apply { 28 binding.apply {
63 radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition 29 radioButton.isChecked = model.selected
64 root.setOnClickListener { 30 root.setOnClickListener {
65 onSelectDriver(bindingAdapterPosition) 31 selectItem(bindingAdapterPosition) {
32 driverViewModel.onDriverSelected(it)
33 driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
34 }
66 } 35 }
67 buttonDelete.setOnClickListener { 36 buttonDelete.setOnClickListener {
68 onDeleteDriver(driverData, bindingAdapterPosition) 37 removeSelectableItem(
38 bindingAdapterPosition
39 ) { removedPosition: Int, selectedPosition: Int ->
40 driverViewModel.onDriverRemoved(removedPosition, selectedPosition)
41 driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
42 }
69 } 43 }
70 44
71 // Delay marquee by 3s 45 // Delay marquee by 3s
@@ -80,38 +54,19 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
80 }, 54 },
81 3000 55 3000
82 ) 56 )
83 if (driver.name == null) { 57 title.text = model.title
84 title.setText(R.string.system_gpu_driver) 58 version.text = model.version
85 description.text = "" 59 description.text = model.description
86 version.text = "" 60 if (model.description.isNotEmpty()) {
87 version.visibility = View.GONE
88 description.visibility = View.GONE
89 buttonDelete.visibility = View.GONE
90 } else {
91 title.text = driver.name
92 version.text = driver.version
93 description.text = driver.description
94 version.visibility = View.VISIBLE 61 version.visibility = View.VISIBLE
95 description.visibility = View.VISIBLE 62 description.visibility = View.VISIBLE
96 buttonDelete.visibility = View.VISIBLE 63 buttonDelete.visibility = View.VISIBLE
64 } else {
65 version.visibility = View.GONE
66 description.visibility = View.GONE
67 buttonDelete.visibility = View.GONE
97 } 68 }
98 } 69 }
99 } 70 }
100 } 71 }
101
102 private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() {
103 override fun areItemsTheSame(
104 oldItem: Pair<String, GpuDriverMetadata>,
105 newItem: Pair<String, GpuDriverMetadata>
106 ): Boolean {
107 return oldItem.first == newItem.first
108 }
109
110 override fun areContentsTheSame(
111 oldItem: Pair<String, GpuDriverMetadata>,
112 newItem: Pair<String, GpuDriverMetadata>
113 ): Boolean {
114 return oldItem.second == newItem.second
115 }
116 }
117} 72}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
index ab657a7b9..3d8f0bda8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
@@ -8,19 +8,14 @@ import android.text.TextUtils
8import android.view.LayoutInflater 8import android.view.LayoutInflater
9import android.view.ViewGroup 9import android.view.ViewGroup
10import androidx.fragment.app.FragmentActivity 10import androidx.fragment.app.FragmentActivity
11import androidx.recyclerview.widget.AsyncDifferConfig
12import androidx.recyclerview.widget.DiffUtil
13import androidx.recyclerview.widget.ListAdapter
14import androidx.recyclerview.widget.RecyclerView
15import org.yuzu.yuzu_emu.databinding.CardFolderBinding 11import org.yuzu.yuzu_emu.databinding.CardFolderBinding
16import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment 12import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
17import org.yuzu.yuzu_emu.model.GameDir 13import org.yuzu.yuzu_emu.model.GameDir
18import org.yuzu.yuzu_emu.model.GamesViewModel 14import org.yuzu.yuzu_emu.model.GamesViewModel
15import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
19 16
20class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : 17class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
21 ListAdapter<GameDir, FolderAdapter.FolderViewHolder>( 18 AbstractDiffAdapter<GameDir, FolderAdapter.FolderViewHolder>() {
22 AsyncDifferConfig.Builder(DiffCallback()).build()
23 ) {
24 override fun onCreateViewHolder( 19 override fun onCreateViewHolder(
25 parent: ViewGroup, 20 parent: ViewGroup,
26 viewType: Int 21 viewType: Int
@@ -29,18 +24,11 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
29 .also { return FolderViewHolder(it) } 24 .also { return FolderViewHolder(it) }
30 } 25 }
31 26
32 override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
33 holder.bind(currentList[position])
34
35 inner class FolderViewHolder(val binding: CardFolderBinding) : 27 inner class FolderViewHolder(val binding: CardFolderBinding) :
36 RecyclerView.ViewHolder(binding.root) { 28 AbstractViewHolder<GameDir>(binding) {
37 private lateinit var gameDir: GameDir 29 override fun bind(model: GameDir) {
38
39 fun bind(gameDir: GameDir) {
40 this.gameDir = gameDir
41
42 binding.apply { 30 binding.apply {
43 path.text = Uri.parse(gameDir.uriString).path 31 path.text = Uri.parse(model.uriString).path
44 path.postDelayed( 32 path.postDelayed(
45 { 33 {
46 path.isSelected = true 34 path.isSelected = true
@@ -50,7 +38,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
50 ) 38 )
51 39
52 buttonEdit.setOnClickListener { 40 buttonEdit.setOnClickListener {
53 GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir) 41 GameFolderPropertiesDialogFragment.newInstance(model)
54 .show( 42 .show(
55 activity.supportFragmentManager, 43 activity.supportFragmentManager,
56 GameFolderPropertiesDialogFragment.TAG 44 GameFolderPropertiesDialogFragment.TAG
@@ -58,19 +46,9 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
58 } 46 }
59 47
60 buttonDelete.setOnClickListener { 48 buttonDelete.setOnClickListener {
61 gamesViewModel.removeFolder(this@FolderViewHolder.gameDir) 49 gamesViewModel.removeFolder(model)
62 } 50 }
63 } 51 }
64 } 52 }
65 } 53 }
66
67 private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
68 override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
69 return oldItem == newItem
70 }
71
72 override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
73 return oldItem == newItem
74 }
75 }
76} 54}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index a578f0de8..e26c2e0ab 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -9,7 +9,6 @@ import android.graphics.drawable.LayerDrawable
9import android.net.Uri 9import android.net.Uri
10import android.text.TextUtils 10import android.text.TextUtils
11import android.view.LayoutInflater 11import android.view.LayoutInflater
12import android.view.View
13import android.view.ViewGroup 12import android.view.ViewGroup
14import android.widget.ImageView 13import android.widget.ImageView
15import android.widget.Toast 14import android.widget.Toast
@@ -25,10 +24,6 @@ import androidx.lifecycle.ViewModelProvider
25import androidx.lifecycle.lifecycleScope 24import androidx.lifecycle.lifecycleScope
26import androidx.navigation.findNavController 25import androidx.navigation.findNavController
27import androidx.preference.PreferenceManager 26import androidx.preference.PreferenceManager
28import androidx.recyclerview.widget.AsyncDifferConfig
29import androidx.recyclerview.widget.DiffUtil
30import androidx.recyclerview.widget.ListAdapter
31import androidx.recyclerview.widget.RecyclerView
32import kotlinx.coroutines.Dispatchers 27import kotlinx.coroutines.Dispatchers
33import kotlinx.coroutines.launch 28import kotlinx.coroutines.launch
34import kotlinx.coroutines.withContext 29import kotlinx.coroutines.withContext
@@ -36,122 +31,26 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections
36import org.yuzu.yuzu_emu.R 31import org.yuzu.yuzu_emu.R
37import org.yuzu.yuzu_emu.YuzuApplication 32import org.yuzu.yuzu_emu.YuzuApplication
38import org.yuzu.yuzu_emu.activities.EmulationActivity 33import org.yuzu.yuzu_emu.activities.EmulationActivity
39import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
40import org.yuzu.yuzu_emu.databinding.CardGameBinding 34import org.yuzu.yuzu_emu.databinding.CardGameBinding
41import org.yuzu.yuzu_emu.model.Game 35import org.yuzu.yuzu_emu.model.Game
42import org.yuzu.yuzu_emu.model.GamesViewModel 36import org.yuzu.yuzu_emu.model.GamesViewModel
43import org.yuzu.yuzu_emu.utils.GameIconUtils 37import org.yuzu.yuzu_emu.utils.GameIconUtils
38import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
44 39
45class GameAdapter(private val activity: AppCompatActivity) : 40class GameAdapter(private val activity: AppCompatActivity) :
46 ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), 41 AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>() {
47 View.OnClickListener,
48 View.OnLongClickListener {
49 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { 42 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
50 // Create a new view. 43 CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
51 val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) 44 .also { return GameViewHolder(it) }
52 binding.cardGame.setOnClickListener(this)
53 binding.cardGame.setOnLongClickListener(this)
54
55 // Use that view to create a ViewHolder.
56 return GameViewHolder(binding)
57 }
58
59 override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
60 holder.bind(currentList[position])
61
62 override fun getItemCount(): Int = currentList.size
63
64 /**
65 * Launches the game that was clicked on.
66 *
67 * @param view The card representing the game the user wants to play.
68 */
69 override fun onClick(view: View) {
70 val holder = view.tag as GameViewHolder
71
72 val gameExists = DocumentFile.fromSingleUri(
73 YuzuApplication.appContext,
74 Uri.parse(holder.game.path)
75 )?.exists() == true
76 if (!gameExists) {
77 Toast.makeText(
78 YuzuApplication.appContext,
79 R.string.loader_error_file_not_found,
80 Toast.LENGTH_LONG
81 ).show()
82
83 ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
84 return
85 }
86
87 val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
88 preferences.edit()
89 .putLong(
90 holder.game.keyLastPlayedTime,
91 System.currentTimeMillis()
92 )
93 .apply()
94
95 val openIntent = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
96 action = Intent.ACTION_VIEW
97 data = Uri.parse(holder.game.path)
98 }
99
100 activity.lifecycleScope.launch {
101 withContext(Dispatchers.IO) {
102 val layerDrawable = ResourcesCompat.getDrawable(
103 YuzuApplication.appContext.resources,
104 R.drawable.shortcut,
105 null
106 ) as LayerDrawable
107 layerDrawable.setDrawableByLayerId(
108 R.id.shortcut_foreground,
109 GameIconUtils.getGameIcon(activity, holder.game)
110 .toDrawable(YuzuApplication.appContext.resources)
111 )
112 val inset = YuzuApplication.appContext.resources
113 .getDimensionPixelSize(R.dimen.icon_inset)
114 layerDrawable.setLayerInset(1, inset, inset, inset, inset)
115 val shortcut =
116 ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path)
117 .setShortLabel(holder.game.title)
118 .setIcon(
119 IconCompat.createWithAdaptiveBitmap(
120 layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
121 )
122 )
123 .setIntent(openIntent)
124 .build()
125 ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
126 }
127 }
128
129 val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
130 view.findNavController().navigate(action)
131 }
132
133 override fun onLongClick(view: View): Boolean {
134 val holder = view.tag as GameViewHolder
135 val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
136 view.findNavController().navigate(action)
137 return true
138 } 45 }
139 46
140 inner class GameViewHolder(val binding: CardGameBinding) : 47 inner class GameViewHolder(val binding: CardGameBinding) :
141 RecyclerView.ViewHolder(binding.root) { 48 AbstractViewHolder<Game>(binding) {
142 lateinit var game: Game 49 override fun bind(model: Game) {
143
144 init {
145 binding.cardGame.tag = this
146 }
147
148 fun bind(game: Game) {
149 this.game = game
150
151 binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP 50 binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
152 GameIconUtils.loadGameIcon(game, binding.imageGameScreen) 51 GameIconUtils.loadGameIcon(model, binding.imageGameScreen)
153 52
154 binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") 53 binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
155 54
156 binding.textGameTitle.postDelayed( 55 binding.textGameTitle.postDelayed(
157 { 56 {
@@ -160,16 +59,79 @@ class GameAdapter(private val activity: AppCompatActivity) :
160 }, 59 },
161 3000 60 3000
162 ) 61 )
62
63 binding.cardGame.setOnClickListener { onClick(model) }
64 binding.cardGame.setOnLongClickListener { onLongClick(model) }
163 } 65 }
164 }
165 66
166 private class DiffCallback : DiffUtil.ItemCallback<Game>() { 67 fun onClick(game: Game) {
167 override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { 68 val gameExists = DocumentFile.fromSingleUri(
168 return oldItem == newItem 69 YuzuApplication.appContext,
70 Uri.parse(game.path)
71 )?.exists() == true
72 if (!gameExists) {
73 Toast.makeText(
74 YuzuApplication.appContext,
75 R.string.loader_error_file_not_found,
76 Toast.LENGTH_LONG
77 ).show()
78
79 ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
80 return
81 }
82
83 val preferences =
84 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
85 preferences.edit()
86 .putLong(
87 game.keyLastPlayedTime,
88 System.currentTimeMillis()
89 )
90 .apply()
91
92 val openIntent =
93 Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
94 action = Intent.ACTION_VIEW
95 data = Uri.parse(game.path)
96 }
97
98 activity.lifecycleScope.launch {
99 withContext(Dispatchers.IO) {
100 val layerDrawable = ResourcesCompat.getDrawable(
101 YuzuApplication.appContext.resources,
102 R.drawable.shortcut,
103 null
104 ) as LayerDrawable
105 layerDrawable.setDrawableByLayerId(
106 R.id.shortcut_foreground,
107 GameIconUtils.getGameIcon(activity, game)
108 .toDrawable(YuzuApplication.appContext.resources)
109 )
110 val inset = YuzuApplication.appContext.resources
111 .getDimensionPixelSize(R.dimen.icon_inset)
112 layerDrawable.setLayerInset(1, inset, inset, inset, inset)
113 val shortcut =
114 ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path)
115 .setShortLabel(game.title)
116 .setIcon(
117 IconCompat.createWithAdaptiveBitmap(
118 layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
119 )
120 )
121 .setIntent(openIntent)
122 .build()
123 ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
124 }
125 }
126
127 val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true)
128 binding.root.findNavController().navigate(action)
169 } 129 }
170 130
171 override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { 131 fun onLongClick(game: Game): Boolean {
172 return oldItem == newItem 132 val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
133 binding.root.findNavController().navigate(action)
134 return true
173 } 135 }
174 } 136 }
175} 137}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
index 95841d786..0046d5314 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
@@ -12,23 +12,22 @@ import androidx.lifecycle.Lifecycle
12import androidx.lifecycle.LifecycleOwner 12import androidx.lifecycle.LifecycleOwner
13import androidx.lifecycle.lifecycleScope 13import androidx.lifecycle.lifecycleScope
14import androidx.lifecycle.repeatOnLifecycle 14import androidx.lifecycle.repeatOnLifecycle
15import androidx.recyclerview.widget.RecyclerView
16import kotlinx.coroutines.launch 15import kotlinx.coroutines.launch
17import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding 16import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
18import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding 17import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
19import org.yuzu.yuzu_emu.model.GameProperty 18import org.yuzu.yuzu_emu.model.GameProperty
20import org.yuzu.yuzu_emu.model.InstallableProperty 19import org.yuzu.yuzu_emu.model.InstallableProperty
21import org.yuzu.yuzu_emu.model.SubmenuProperty 20import org.yuzu.yuzu_emu.model.SubmenuProperty
21import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
22 22
23class GamePropertiesAdapter( 23class GamePropertiesAdapter(
24 private val viewLifecycle: LifecycleOwner, 24 private val viewLifecycle: LifecycleOwner,
25 private var properties: List<GameProperty> 25 private var properties: List<GameProperty>
26) : 26) : AbstractListAdapter<GameProperty, AbstractViewHolder<GameProperty>>(properties) {
27 RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() {
28 override fun onCreateViewHolder( 27 override fun onCreateViewHolder(
29 parent: ViewGroup, 28 parent: ViewGroup,
30 viewType: Int 29 viewType: Int
31 ): GamePropertyViewHolder { 30 ): AbstractViewHolder<GameProperty> {
32 val inflater = LayoutInflater.from(parent.context) 31 val inflater = LayoutInflater.from(parent.context)
33 return when (viewType) { 32 return when (viewType) {
34 PropertyType.Submenu.ordinal -> { 33 PropertyType.Submenu.ordinal -> {
@@ -51,11 +50,6 @@ class GamePropertiesAdapter(
51 } 50 }
52 } 51 }
53 52
54 override fun getItemCount(): Int = properties.size
55
56 override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) =
57 holder.bind(properties[position])
58
59 override fun getItemViewType(position: Int): Int { 53 override fun getItemViewType(position: Int): Int {
60 return when (properties[position]) { 54 return when (properties[position]) {
61 is SubmenuProperty -> PropertyType.Submenu.ordinal 55 is SubmenuProperty -> PropertyType.Submenu.ordinal
@@ -63,14 +57,10 @@ class GamePropertiesAdapter(
63 } 57 }
64 } 58 }
65 59
66 sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
67 abstract fun bind(property: GameProperty)
68 }
69
70 inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) : 60 inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) :
71 GamePropertyViewHolder(binding.root) { 61 AbstractViewHolder<GameProperty>(binding) {
72 override fun bind(property: GameProperty) { 62 override fun bind(model: GameProperty) {
73 val submenuProperty = property as SubmenuProperty 63 val submenuProperty = model as SubmenuProperty
74 64
75 binding.root.setOnClickListener { 65 binding.root.setOnClickListener {
76 submenuProperty.action.invoke() 66 submenuProperty.action.invoke()
@@ -108,9 +98,9 @@ class GamePropertiesAdapter(
108 } 98 }
109 99
110 inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) : 100 inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) :
111 GamePropertyViewHolder(binding.root) { 101 AbstractViewHolder<GameProperty>(binding) {
112 override fun bind(property: GameProperty) { 102 override fun bind(model: GameProperty) {
113 val installableProperty = property as InstallableProperty 103 val installableProperty = model as InstallableProperty
114 104
115 binding.title.setText(installableProperty.titleId) 105 binding.title.setText(installableProperty.titleId)
116 binding.description.setText(installableProperty.descriptionId) 106 binding.description.setText(installableProperty.descriptionId)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
index 58ce343f4..b512845d5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
@@ -14,69 +14,37 @@ import androidx.lifecycle.Lifecycle
14import androidx.lifecycle.LifecycleOwner 14import androidx.lifecycle.LifecycleOwner
15import androidx.lifecycle.lifecycleScope 15import androidx.lifecycle.lifecycleScope
16import androidx.lifecycle.repeatOnLifecycle 16import androidx.lifecycle.repeatOnLifecycle
17import androidx.recyclerview.widget.RecyclerView
18import kotlinx.coroutines.launch 17import kotlinx.coroutines.launch
19import org.yuzu.yuzu_emu.R 18import org.yuzu.yuzu_emu.R
20import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding 19import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
21import org.yuzu.yuzu_emu.fragments.MessageDialogFragment 20import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
22import org.yuzu.yuzu_emu.model.HomeSetting 21import org.yuzu.yuzu_emu.model.HomeSetting
22import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
23 23
24class HomeSettingAdapter( 24class HomeSettingAdapter(
25 private val activity: AppCompatActivity, 25 private val activity: AppCompatActivity,
26 private val viewLifecycle: LifecycleOwner, 26 private val viewLifecycle: LifecycleOwner,
27 var options: List<HomeSetting> 27 options: List<HomeSetting>
28) : 28) : AbstractListAdapter<HomeSetting, HomeSettingAdapter.HomeOptionViewHolder>(options) {
29 RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(),
30 View.OnClickListener {
31 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { 29 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
32 val binding = 30 CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
33 CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) 31 .also { return HomeOptionViewHolder(it) }
34 binding.root.setOnClickListener(this)
35 return HomeOptionViewHolder(binding)
36 }
37
38 override fun getItemCount(): Int {
39 return options.size
40 }
41
42 override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
43 holder.bind(options[position])
44 }
45
46 override fun onClick(view: View) {
47 val holder = view.tag as HomeOptionViewHolder
48 if (holder.option.isEnabled.invoke()) {
49 holder.option.onClick.invoke()
50 } else {
51 MessageDialogFragment.newInstance(
52 activity,
53 titleId = holder.option.disabledTitleId,
54 descriptionId = holder.option.disabledMessageId
55 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
56 }
57 } 32 }
58 33
59 inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) : 34 inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
60 RecyclerView.ViewHolder(binding.root) { 35 AbstractViewHolder<HomeSetting>(binding) {
61 lateinit var option: HomeSetting 36 override fun bind(model: HomeSetting) {
62 37 binding.optionTitle.text = activity.resources.getString(model.titleId)
63 init { 38 binding.optionDescription.text = activity.resources.getString(model.descriptionId)
64 itemView.tag = this
65 }
66
67 fun bind(option: HomeSetting) {
68 this.option = option
69 binding.optionTitle.text = activity.resources.getString(option.titleId)
70 binding.optionDescription.text = activity.resources.getString(option.descriptionId)
71 binding.optionIcon.setImageDrawable( 39 binding.optionIcon.setImageDrawable(
72 ResourcesCompat.getDrawable( 40 ResourcesCompat.getDrawable(
73 activity.resources, 41 activity.resources,
74 option.iconId, 42 model.iconId,
75 activity.theme 43 activity.theme
76 ) 44 )
77 ) 45 )
78 46
79 when (option.titleId) { 47 when (model.titleId) {
80 R.string.get_early_access -> 48 R.string.get_early_access ->
81 binding.optionLayout.background = 49 binding.optionLayout.background =
82 ContextCompat.getDrawable( 50 ContextCompat.getDrawable(
@@ -85,7 +53,7 @@ class HomeSettingAdapter(
85 ) 53 )
86 } 54 }
87 55
88 if (!option.isEnabled.invoke()) { 56 if (!model.isEnabled.invoke()) {
89 binding.optionTitle.alpha = 0.5f 57 binding.optionTitle.alpha = 0.5f
90 binding.optionDescription.alpha = 0.5f 58 binding.optionDescription.alpha = 0.5f
91 binding.optionIcon.alpha = 0.5f 59 binding.optionIcon.alpha = 0.5f
@@ -93,7 +61,7 @@ class HomeSettingAdapter(
93 61
94 viewLifecycle.lifecycleScope.launch { 62 viewLifecycle.lifecycleScope.launch {
95 viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { 63 viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
96 option.details.collect { updateOptionDetails(it) } 64 model.details.collect { updateOptionDetails(it) }
97 } 65 }
98 } 66 }
99 binding.optionDetail.postDelayed( 67 binding.optionDetail.postDelayed(
@@ -103,6 +71,20 @@ class HomeSettingAdapter(
103 }, 71 },
104 3000 72 3000
105 ) 73 )
74
75 binding.root.setOnClickListener { onClick(model) }
76 }
77
78 private fun onClick(model: HomeSetting) {
79 if (model.isEnabled.invoke()) {
80 model.onClick.invoke()
81 } else {
82 MessageDialogFragment.newInstance(
83 activity,
84 titleId = model.disabledTitleId,
85 descriptionId = model.disabledMessageId
86 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
87 }
106 } 88 }
107 89
108 private fun updateOptionDetails(detailString: String) { 90 private fun updateOptionDetails(detailString: String) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
index e960fbaab..4218c4e52 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
@@ -6,43 +6,33 @@ package org.yuzu.yuzu_emu.adapters
6import android.view.LayoutInflater 6import android.view.LayoutInflater
7import android.view.View 7import android.view.View
8import android.view.ViewGroup 8import android.view.ViewGroup
9import androidx.recyclerview.widget.RecyclerView
10import org.yuzu.yuzu_emu.databinding.CardInstallableBinding 9import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
11import org.yuzu.yuzu_emu.model.Installable 10import org.yuzu.yuzu_emu.model.Installable
11import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
12 12
13class InstallableAdapter(private val installables: List<Installable>) : 13class InstallableAdapter(installables: List<Installable>) :
14 RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { 14 AbstractListAdapter<Installable, InstallableAdapter.InstallableViewHolder>(installables) {
15 override fun onCreateViewHolder( 15 override fun onCreateViewHolder(
16 parent: ViewGroup, 16 parent: ViewGroup,
17 viewType: Int 17 viewType: Int
18 ): InstallableAdapter.InstallableViewHolder { 18 ): InstallableAdapter.InstallableViewHolder {
19 val binding = 19 CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
20 CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) 20 .also { return InstallableViewHolder(it) }
21 return InstallableViewHolder(binding)
22 } 21 }
23 22
24 override fun getItemCount(): Int = installables.size
25
26 override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) =
27 holder.bind(installables[position])
28
29 inner class InstallableViewHolder(val binding: CardInstallableBinding) : 23 inner class InstallableViewHolder(val binding: CardInstallableBinding) :
30 RecyclerView.ViewHolder(binding.root) { 24 AbstractViewHolder<Installable>(binding) {
31 lateinit var installable: Installable 25 override fun bind(model: Installable) {
32 26 binding.title.setText(model.titleId)
33 fun bind(installable: Installable) { 27 binding.description.setText(model.descriptionId)
34 this.installable = installable
35
36 binding.title.setText(installable.titleId)
37 binding.description.setText(installable.descriptionId)
38 28
39 if (installable.install != null) { 29 if (model.install != null) {
40 binding.buttonInstall.visibility = View.VISIBLE 30 binding.buttonInstall.visibility = View.VISIBLE
41 binding.buttonInstall.setOnClickListener { installable.install.invoke() } 31 binding.buttonInstall.setOnClickListener { model.install.invoke() }
42 } 32 }
43 if (installable.export != null) { 33 if (model.export != null) {
44 binding.buttonExport.visibility = View.VISIBLE 34 binding.buttonExport.visibility = View.VISIBLE
45 binding.buttonExport.setOnClickListener { installable.export.invoke() } 35 binding.buttonExport.setOnClickListener { model.export.invoke() }
46 } 36 }
47 } 37 }
48 } 38 }
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 bc6ff1364..38bb1f96f 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
@@ -7,49 +7,33 @@ import android.view.LayoutInflater
7import android.view.View 7import android.view.View
8import android.view.ViewGroup 8import android.view.ViewGroup
9import androidx.appcompat.app.AppCompatActivity 9import androidx.appcompat.app.AppCompatActivity
10import androidx.recyclerview.widget.RecyclerView
11import androidx.recyclerview.widget.RecyclerView.ViewHolder
12import org.yuzu.yuzu_emu.YuzuApplication
13import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding 10import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
14import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment 11import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
15import org.yuzu.yuzu_emu.model.License 12import org.yuzu.yuzu_emu.model.License
13import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
16 14
17class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) : 15class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
18 RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(), 16 AbstractListAdapter<License, LicenseAdapter.LicenseViewHolder>(licenses) {
19 View.OnClickListener {
20 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder { 17 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
21 val binding = 18 ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
22 ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false) 19 .also { return LicenseViewHolder(it) }
23 binding.root.setOnClickListener(this)
24 return LicenseViewHolder(binding)
25 } 20 }
26 21
27 override fun getItemCount(): Int = licenses.size 22 inner class LicenseViewHolder(val binding: ListItemSettingBinding) :
23 AbstractViewHolder<License>(binding) {
24 override fun bind(model: License) {
25 binding.apply {
26 textSettingName.text = root.context.getString(model.titleId)
27 textSettingDescription.text = root.context.getString(model.descriptionId)
28 textSettingValue.visibility = View.GONE
28 29
29 override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) { 30 root.setOnClickListener { onClick(model) }
30 holder.bind(licenses[position]) 31 }
31 }
32
33 override fun onClick(view: View) {
34 val license = (view.tag as LicenseViewHolder).license
35 LicenseBottomSheetDialogFragment.newInstance(license)
36 .show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
37 }
38
39 inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) {
40 lateinit var license: License
41
42 init {
43 itemView.tag = this
44 } 32 }
45 33
46 fun bind(license: License) { 34 private fun onClick(license: License) {
47 this.license = license 35 LicenseBottomSheetDialogFragment.newInstance(license)
48 36 .show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
49 val context = YuzuApplication.appContext
50 binding.textSettingName.text = context.getString(license.titleId)
51 binding.textSettingDescription.text = context.getString(license.descriptionId)
52 binding.textSettingValue.visibility = View.GONE
53 } 37 }
54 } 38 }
55} 39}
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 6b46d359e..02118e1a8 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
@@ -10,7 +10,6 @@ import android.view.ViewGroup
10import androidx.appcompat.app.AppCompatActivity 10import androidx.appcompat.app.AppCompatActivity
11import androidx.core.content.res.ResourcesCompat 11import androidx.core.content.res.ResourcesCompat
12import androidx.lifecycle.ViewModelProvider 12import androidx.lifecycle.ViewModelProvider
13import androidx.recyclerview.widget.RecyclerView
14import com.google.android.material.button.MaterialButton 13import com.google.android.material.button.MaterialButton
15import org.yuzu.yuzu_emu.databinding.PageSetupBinding 14import org.yuzu.yuzu_emu.databinding.PageSetupBinding
16import org.yuzu.yuzu_emu.model.HomeViewModel 15import org.yuzu.yuzu_emu.model.HomeViewModel
@@ -18,31 +17,19 @@ import org.yuzu.yuzu_emu.model.SetupCallback
18import org.yuzu.yuzu_emu.model.SetupPage 17import org.yuzu.yuzu_emu.model.SetupPage
19import org.yuzu.yuzu_emu.model.StepState 18import org.yuzu.yuzu_emu.model.StepState
20import org.yuzu.yuzu_emu.utils.ViewUtils 19import org.yuzu.yuzu_emu.utils.ViewUtils
20import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
21 21
22class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) : 22class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
23 RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() { 23 AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) {
24 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder { 24 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
25 val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false) 25 PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
26 return SetupPageViewHolder(binding) 26 .also { return SetupPageViewHolder(it) }
27 } 27 }
28 28
29 override fun getItemCount(): Int = pages.size
30
31 override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
32 holder.bind(pages[position])
33
34 inner class SetupPageViewHolder(val binding: PageSetupBinding) : 29 inner class SetupPageViewHolder(val binding: PageSetupBinding) :
35 RecyclerView.ViewHolder(binding.root), SetupCallback { 30 AbstractViewHolder<SetupPage>(binding), SetupCallback {
36 lateinit var page: SetupPage 31 override fun bind(model: SetupPage) {
37 32 if (model.stepCompleted.invoke() == StepState.COMPLETE) {
38 init {
39 itemView.tag = this
40 }
41
42 fun bind(page: SetupPage) {
43 this.page = page
44
45 if (page.stepCompleted.invoke() == StepState.COMPLETE) {
46 binding.buttonAction.visibility = View.INVISIBLE 33 binding.buttonAction.visibility = View.INVISIBLE
47 binding.textConfirmation.visibility = View.VISIBLE 34 binding.textConfirmation.visibility = View.VISIBLE
48 } 35 }
@@ -50,31 +37,31 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
50 binding.icon.setImageDrawable( 37 binding.icon.setImageDrawable(
51 ResourcesCompat.getDrawable( 38 ResourcesCompat.getDrawable(
52 activity.resources, 39 activity.resources,
53 page.iconId, 40 model.iconId,
54 activity.theme 41 activity.theme
55 ) 42 )
56 ) 43 )
57 binding.textTitle.text = activity.resources.getString(page.titleId) 44 binding.textTitle.text = activity.resources.getString(model.titleId)
58 binding.textDescription.text = 45 binding.textDescription.text =
59 Html.fromHtml(activity.resources.getString(page.descriptionId), 0) 46 Html.fromHtml(activity.resources.getString(model.descriptionId), 0)
60 47
61 binding.buttonAction.apply { 48 binding.buttonAction.apply {
62 text = activity.resources.getString(page.buttonTextId) 49 text = activity.resources.getString(model.buttonTextId)
63 if (page.buttonIconId != 0) { 50 if (model.buttonIconId != 0) {
64 icon = ResourcesCompat.getDrawable( 51 icon = ResourcesCompat.getDrawable(
65 activity.resources, 52 activity.resources,
66 page.buttonIconId, 53 model.buttonIconId,
67 activity.theme 54 activity.theme
68 ) 55 )
69 } 56 }
70 iconGravity = 57 iconGravity =
71 if (page.leftAlignedIcon) { 58 if (model.leftAlignedIcon) {
72 MaterialButton.ICON_GRAVITY_START 59 MaterialButton.ICON_GRAVITY_START
73 } else { 60 } else {
74 MaterialButton.ICON_GRAVITY_END 61 MaterialButton.ICON_GRAVITY_END
75 } 62 }
76 setOnClickListener { 63 setOnClickListener {
77 page.buttonAction.invoke(this@SetupPageViewHolder) 64 model.buttonAction.invoke(this@SetupPageViewHolder)
78 } 65 }
79 } 66 }
80 } 67 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
index cc71254dc..9dabb9c41 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -3,6 +3,7 @@
3 3
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.fragments
5 5
6import android.annotation.SuppressLint
6import android.os.Bundle 7import android.os.Bundle
7import android.view.LayoutInflater 8import android.view.LayoutInflater
8import android.view.View 9import android.view.View
@@ -13,20 +14,26 @@ import androidx.core.view.WindowInsetsCompat
13import androidx.core.view.updatePadding 14import androidx.core.view.updatePadding
14import androidx.fragment.app.Fragment 15import androidx.fragment.app.Fragment
15import androidx.fragment.app.activityViewModels 16import androidx.fragment.app.activityViewModels
17import androidx.lifecycle.Lifecycle
16import androidx.lifecycle.lifecycleScope 18import androidx.lifecycle.lifecycleScope
19import androidx.lifecycle.repeatOnLifecycle
17import androidx.navigation.findNavController 20import androidx.navigation.findNavController
18import androidx.navigation.fragment.navArgs 21import androidx.navigation.fragment.navArgs
19import androidx.recyclerview.widget.GridLayoutManager 22import androidx.recyclerview.widget.GridLayoutManager
20import com.google.android.material.transition.MaterialSharedAxis 23import com.google.android.material.transition.MaterialSharedAxis
21import kotlinx.coroutines.flow.collectLatest 24import kotlinx.coroutines.Dispatchers
22import kotlinx.coroutines.launch 25import kotlinx.coroutines.launch
26import kotlinx.coroutines.withContext
23import org.yuzu.yuzu_emu.R 27import org.yuzu.yuzu_emu.R
24import org.yuzu.yuzu_emu.adapters.DriverAdapter 28import org.yuzu.yuzu_emu.adapters.DriverAdapter
25import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding 29import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
30import org.yuzu.yuzu_emu.features.settings.model.StringSetting
31import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
26import org.yuzu.yuzu_emu.model.DriverViewModel 32import org.yuzu.yuzu_emu.model.DriverViewModel
27import org.yuzu.yuzu_emu.model.HomeViewModel 33import org.yuzu.yuzu_emu.model.HomeViewModel
28import org.yuzu.yuzu_emu.utils.FileUtil 34import org.yuzu.yuzu_emu.utils.FileUtil
29import org.yuzu.yuzu_emu.utils.GpuDriverHelper 35import org.yuzu.yuzu_emu.utils.GpuDriverHelper
36import org.yuzu.yuzu_emu.utils.NativeConfig
30import java.io.File 37import java.io.File
31import java.io.IOException 38import java.io.IOException
32 39
@@ -55,12 +62,43 @@ class DriverManagerFragment : Fragment() {
55 return binding.root 62 return binding.root
56 } 63 }
57 64
65 // This is using the correct scope, lint is just acting up
66 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
58 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 67 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
59 super.onViewCreated(view, savedInstanceState) 68 super.onViewCreated(view, savedInstanceState)
60 homeViewModel.setNavigationVisibility(visible = false, animated = true) 69 homeViewModel.setNavigationVisibility(visible = false, animated = true)
61 homeViewModel.setStatusBarShadeVisibility(visible = false) 70 homeViewModel.setStatusBarShadeVisibility(visible = false)
62 71
63 driverViewModel.onOpenDriverManager(args.game) 72 driverViewModel.onOpenDriverManager(args.game)
73 if (NativeConfig.isPerGameConfigLoaded()) {
74 binding.toolbarDrivers.inflateMenu(R.menu.menu_driver_manager)
75 driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
76 binding.toolbarDrivers.setOnMenuItemClickListener {
77 when (it.itemId) {
78 R.id.menu_driver_clear -> {
79 StringSetting.DRIVER_PATH.global = true
80 driverViewModel.updateDriverList()
81 (binding.listDrivers.adapter as DriverAdapter)
82 .replaceList(driverViewModel.driverList.value)
83 driverViewModel.showClearButton(false)
84 true
85 }
86
87 else -> false
88 }
89 }
90
91 viewLifecycleOwner.lifecycleScope.apply {
92 launch {
93 repeatOnLifecycle(Lifecycle.State.STARTED) {
94 driverViewModel.showClearButton.collect {
95 binding.toolbarDrivers.menu
96 .findItem(R.id.menu_driver_clear).isVisible = it
97 }
98 }
99 }
100 }
101 }
64 102
65 if (!driverViewModel.isInteractionAllowed.value) { 103 if (!driverViewModel.isInteractionAllowed.value) {
66 DriversLoadingDialogFragment().show( 104 DriversLoadingDialogFragment().show(
@@ -85,25 +123,6 @@ class DriverManagerFragment : Fragment() {
85 adapter = DriverAdapter(driverViewModel) 123 adapter = DriverAdapter(driverViewModel)
86 } 124 }
87 125
88 viewLifecycleOwner.lifecycleScope.apply {
89 launch {
90 driverViewModel.driverList.collectLatest {
91 (binding.listDrivers.adapter as DriverAdapter).submitList(it)
92 }
93 }
94 launch {
95 driverViewModel.newDriverInstalled.collect {
96 if (_binding != null && it) {
97 (binding.listDrivers.adapter as DriverAdapter).apply {
98 notifyItemChanged(driverViewModel.previouslySelectedDriver)
99 notifyItemChanged(driverViewModel.selectedDriver)
100 driverViewModel.setNewDriverInstalled(false)
101 }
102 }
103 }
104 }
105 }
106
107 setInsets() 126 setInsets()
108 } 127 }
109 128
@@ -160,7 +179,7 @@ class DriverManagerFragment : Fragment() {
160 false 179 false
161 ) { 180 ) {
162 val driverPath = 181 val driverPath =
163 "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}" 182 "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"
164 val driverFile = File(driverPath) 183 val driverFile = File(driverPath)
165 184
166 // Ignore file exceptions when a user selects an invalid zip 185 // Ignore file exceptions when a user selects an invalid zip
@@ -177,12 +196,21 @@ class DriverManagerFragment : Fragment() {
177 196
178 val driverData = GpuDriverHelper.getMetadataFromZip(driverFile) 197 val driverData = GpuDriverHelper.getMetadataFromZip(driverFile)
179 val driverInList = 198 val driverInList =
180 driverViewModel.driverList.value.firstOrNull { it.second == driverData } 199 driverViewModel.driverData.firstOrNull { it.second == driverData }
181 if (driverInList != null) { 200 if (driverInList != null) {
182 return@newInstance getString(R.string.driver_already_installed) 201 return@newInstance getString(R.string.driver_already_installed)
183 } else { 202 } else {
184 driverViewModel.addDriver(Pair(driverPath, driverData)) 203 driverViewModel.onDriverAdded(Pair(driverPath, driverData))
185 driverViewModel.setNewDriverInstalled(true) 204 withContext(Dispatchers.Main) {
205 if (_binding != null) {
206 val adapter = binding.listDrivers.adapter as DriverAdapter
207 adapter.addItem(driverData.toDriver())
208 adapter.selectItem(adapter.currentList.indices.last)
209 driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
210 binding.listDrivers
211 .smoothScrollToPosition(adapter.currentList.indices.last)
212 }
213 }
186 } 214 }
187 return@newInstance Any() 215 return@newInstance Any()
188 }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) 216 }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Driver.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Driver.kt
new file mode 100644
index 000000000..de342212a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Driver.kt
@@ -0,0 +1,27 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
7
8data class Driver(
9 override var selected: Boolean,
10 val title: String,
11 val version: String = "",
12 val description: String = ""
13) : SelectableItem {
14 override fun onSelectionStateChanged(selected: Boolean) {
15 this.selected = selected
16 }
17
18 companion object {
19 fun GpuDriverMetadata.toDriver(selected: Boolean = false): Driver =
20 Driver(
21 selected,
22 this.name ?: "",
23 this.version ?: "",
24 this.description ?: ""
25 )
26 }
27}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
index 76accf8f3..15ae3a42b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
@@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
9import kotlinx.coroutines.flow.MutableStateFlow 9import kotlinx.coroutines.flow.MutableStateFlow
10import kotlinx.coroutines.flow.SharingStarted 10import kotlinx.coroutines.flow.SharingStarted
11import kotlinx.coroutines.flow.StateFlow 11import kotlinx.coroutines.flow.StateFlow
12import kotlinx.coroutines.flow.asStateFlow
12import kotlinx.coroutines.flow.combine 13import kotlinx.coroutines.flow.combine
13import kotlinx.coroutines.flow.stateIn 14import kotlinx.coroutines.flow.stateIn
14import kotlinx.coroutines.launch 15import kotlinx.coroutines.launch
@@ -17,11 +18,10 @@ import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.YuzuApplication 18import org.yuzu.yuzu_emu.YuzuApplication
18import org.yuzu.yuzu_emu.features.settings.model.StringSetting 19import org.yuzu.yuzu_emu.features.settings.model.StringSetting
19import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 20import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
20import org.yuzu.yuzu_emu.utils.FileUtil 21import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
21import org.yuzu.yuzu_emu.utils.GpuDriverHelper 22import org.yuzu.yuzu_emu.utils.GpuDriverHelper
22import org.yuzu.yuzu_emu.utils.GpuDriverMetadata 23import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
23import org.yuzu.yuzu_emu.utils.NativeConfig 24import org.yuzu.yuzu_emu.utils.NativeConfig
24import java.io.BufferedOutputStream
25import java.io.File 25import java.io.File
26 26
27class DriverViewModel : ViewModel() { 27class DriverViewModel : ViewModel() {
@@ -38,97 +38,81 @@ class DriverViewModel : ViewModel() {
38 !loading && ready && !deleting 38 !loading && ready && !deleting
39 }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false) 39 }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false)
40 40
41 private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers()) 41 var driverData = GpuDriverHelper.getDrivers()
42 val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
43 42
44 var previouslySelectedDriver = 0 43 private val _driverList = MutableStateFlow(emptyList<Driver>())
45 var selectedDriver = -1 44 val driverList: StateFlow<List<Driver>> get() = _driverList
46 45
47 // Used for showing which driver is currently installed within the driver manager card 46 // Used for showing which driver is currently installed within the driver manager card
48 private val _selectedDriverTitle = MutableStateFlow("") 47 private val _selectedDriverTitle = MutableStateFlow("")
49 val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle 48 val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
50 49
51 private val _newDriverInstalled = MutableStateFlow(false) 50 private val _showClearButton = MutableStateFlow(false)
52 val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled 51 val showClearButton = _showClearButton.asStateFlow()
53 52
54 val driversToDelete = mutableListOf<String>() 53 private val driversToDelete = mutableListOf<String>()
55 54
56 init { 55 init {
57 val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData 56 updateDriverList()
58 findSelectedDriver(currentDriverMetadata) 57 updateDriverNameForGame(null)
59 58 }
60 // If a user had installed a driver before the manager was implemented, this zips
61 // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
62 // be indexed and exported as expected.
63 if (selectedDriver == -1) {
64 val driverToSave =
65 File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
66 driverToSave.createNewFile()
67 FileUtil.zipFromInternalStorage(
68 File(GpuDriverHelper.driverInstallationPath!!),
69 GpuDriverHelper.driverInstallationPath!!,
70 BufferedOutputStream(driverToSave.outputStream())
71 )
72 _driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
73 setSelectedDriverIndex(_driverList.value.size - 1)
74 }
75 59
76 // If a user had installed a driver before the config was reworked to be multiplatform, 60 fun reloadDriverData() {
77 // we have save the path of the previously selected driver to the new setting. 61 _areDriversLoading.value = true
78 if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 && 62 driverData = GpuDriverHelper.getDrivers()
79 StringSetting.DRIVER_PATH.global 63 updateDriverList()
80 ) { 64 _areDriversLoading.value = false
81 StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first) 65 }
82 NativeConfig.saveGlobalConfig() 66
83 } else { 67 fun updateDriverList() {
84 findSelectedDriver(GpuDriverHelper.customDriverSettingData) 68 val selectedDriver = GpuDriverHelper.customDriverSettingData
69 val newDriverList = mutableListOf(
70 Driver(
71 selectedDriver == GpuDriverMetadata(),
72 YuzuApplication.appContext.getString(R.string.system_gpu_driver)
73 )
74 )
75 driverData.forEach {
76 newDriverList.add(it.second.toDriver(it.second == selectedDriver))
85 } 77 }
86 updateDriverNameForGame(null) 78 _driverList.value = newDriverList
87 } 79 }
88 80
89 fun setSelectedDriverIndex(value: Int) { 81 fun onOpenDriverManager(game: Game?) {
90 if (selectedDriver != -1) { 82 if (game != null) {
91 previouslySelectedDriver = selectedDriver 83 SettingsFile.loadCustomConfig(game)
92 } 84 }
93 selectedDriver = value 85 updateDriverList()
94 } 86 }
95 87
96 fun setNewDriverInstalled(value: Boolean) { 88 fun showClearButton(value: Boolean) {
97 _newDriverInstalled.value = value 89 _showClearButton.value = value
98 } 90 }
99 91
100 fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { 92 fun onDriverSelected(position: Int) {
101 val driverIndex = _driverList.value.indexOfFirst { it == driverData } 93 if (position == 0) {
102 if (driverIndex == -1) { 94 StringSetting.DRIVER_PATH.setString("")
103 _driverList.value.add(driverData)
104 setSelectedDriverIndex(_driverList.value.size - 1)
105 _selectedDriverTitle.value = driverData.second.name
106 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
107 } else { 95 } else {
108 setSelectedDriverIndex(driverIndex) 96 StringSetting.DRIVER_PATH.setString(driverData[position - 1].first)
109 } 97 }
110 } 98 }
111 99
112 fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) { 100 fun onDriverRemoved(removedPosition: Int, selectedPosition: Int) {
113 _driverList.value.remove(driverData) 101 driversToDelete.add(driverData[removedPosition - 1].first)
102 driverData.removeAt(removedPosition - 1)
103 onDriverSelected(selectedPosition)
114 } 104 }
115 105
116 fun onOpenDriverManager(game: Game?) { 106 fun onDriverAdded(driver: Pair<String, GpuDriverMetadata>) {
117 if (game != null) { 107 if (driversToDelete.contains(driver.first)) {
118 SettingsFile.loadCustomConfig(game) 108 driversToDelete.remove(driver.first)
119 }
120
121 val driverPath = StringSetting.DRIVER_PATH.getString()
122 if (driverPath.isEmpty()) {
123 setSelectedDriverIndex(0)
124 } else {
125 findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath)))
126 } 109 }
110 driverData.add(driver)
111 onDriverSelected(driverData.size)
127 } 112 }
128 113
129 fun onCloseDriverManager(game: Game?) { 114 fun onCloseDriverManager(game: Game?) {
130 _isDeletingDrivers.value = true 115 _isDeletingDrivers.value = true
131 StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)
132 updateDriverNameForGame(game) 116 updateDriverNameForGame(game)
133 if (game == null) { 117 if (game == null) {
134 NativeConfig.saveGlobalConfig() 118 NativeConfig.saveGlobalConfig()
@@ -181,20 +165,6 @@ class DriverViewModel : ViewModel() {
181 } 165 }
182 } 166 }
183 167
184 private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) {
185 if (driverList.value.size == 1) {
186 setSelectedDriverIndex(0)
187 return
188 }
189
190 driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> ->
191 if (driver.second == currentDriverMetadata) {
192 setSelectedDriverIndex(i)
193 return
194 }
195 }
196 }
197
198 fun updateDriverNameForGame(game: Game?) { 168 fun updateDriverNameForGame(game: Game?) {
199 if (!GpuDriverHelper.supportsCustomDriverLoading()) { 169 if (!GpuDriverHelper.supportsCustomDriverLoading()) {
200 return 170 return
@@ -217,7 +187,6 @@ class DriverViewModel : ViewModel() {
217 187
218 private fun setDriverReady() { 188 private fun setDriverReady() {
219 _isDriverReady.value = true 189 _isDriverReady.value = true
220 _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name 190 updateName()
221 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
222 } 191 }
223} 192}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SelectableItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SelectableItem.kt
new file mode 100644
index 000000000..11c22d323
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SelectableItem.kt
@@ -0,0 +1,9 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6interface SelectableItem {
7 var selected: Boolean
8 fun onSelectionStateChanged(selected: Boolean)
9}
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 622ae996e..644289e25 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
@@ -41,6 +41,7 @@ import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
41import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment 41import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
42import org.yuzu.yuzu_emu.fragments.MessageDialogFragment 42import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
43import org.yuzu.yuzu_emu.model.AddonViewModel 43import org.yuzu.yuzu_emu.model.AddonViewModel
44import org.yuzu.yuzu_emu.model.DriverViewModel
44import org.yuzu.yuzu_emu.model.GamesViewModel 45import org.yuzu.yuzu_emu.model.GamesViewModel
45import org.yuzu.yuzu_emu.model.HomeViewModel 46import org.yuzu.yuzu_emu.model.HomeViewModel
46import org.yuzu.yuzu_emu.model.TaskState 47import org.yuzu.yuzu_emu.model.TaskState
@@ -58,6 +59,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
58 private val gamesViewModel: GamesViewModel by viewModels() 59 private val gamesViewModel: GamesViewModel by viewModels()
59 private val taskViewModel: TaskViewModel by viewModels() 60 private val taskViewModel: TaskViewModel by viewModels()
60 private val addonViewModel: AddonViewModel by viewModels() 61 private val addonViewModel: AddonViewModel by viewModels()
62 private val driverViewModel: DriverViewModel by viewModels()
61 63
62 override var themeId: Int = 0 64 override var themeId: Int = 0
63 65
@@ -689,6 +691,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
689 NativeLibrary.initializeSystem(true) 691 NativeLibrary.initializeSystem(true)
690 NativeConfig.initializeGlobalConfig() 692 NativeConfig.initializeGlobalConfig()
691 gamesViewModel.reloadGames(false) 693 gamesViewModel.reloadGames(false)
694 driverViewModel.reloadDriverData()
692 695
693 return@newInstance getString(R.string.user_data_import_success) 696 return@newInstance getString(R.string.user_data_import_success)
694 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 697 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
index 685272288..a8f9dcc34 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -62,9 +62,6 @@ object GpuDriverHelper {
62 ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name } 62 ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }
63 ?.distinct() 63 ?.distinct()
64 ?.toMutableList() ?: mutableListOf() 64 ?.toMutableList() ?: mutableListOf()
65
66 // TODO: Get system driver information
67 drivers.add(0, Pair("", GpuDriverMetadata()))
68 return drivers 65 return drivers
69 } 66 }
70 67
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholder/AbstractViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholder/AbstractViewHolder.kt
new file mode 100644
index 000000000..7101ad434
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholder/AbstractViewHolder.kt
@@ -0,0 +1,18 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.viewholder
5
6import androidx.recyclerview.widget.RecyclerView
7import androidx.viewbinding.ViewBinding
8import org.yuzu.yuzu_emu.adapters.AbstractDiffAdapter
9import org.yuzu.yuzu_emu.adapters.AbstractListAdapter
10
11/**
12 * [RecyclerView.ViewHolder] meant to work together with a [AbstractDiffAdapter] or a
13 * [AbstractListAdapter] so we can run [bind] on each list item without needing a manual hookup.
14 */
15abstract class AbstractViewHolder<Model>(binding: ViewBinding) :
16 RecyclerView.ViewHolder(binding.root) {
17 abstract fun bind(model: Model)
18}
diff --git a/src/android/app/src/main/res/menu/menu_driver_manager.xml b/src/android/app/src/main/res/menu/menu_driver_manager.xml
new file mode 100644
index 000000000..dee5d57b6
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_driver_manager.xml
@@ -0,0 +1,11 @@
1<?xml version="1.0" encoding="utf-8"?>
2<menu xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto">
4
5 <item
6 android:id="@+id/menu_driver_clear"
7 android:icon="@drawable/ic_clear"
8 android:title="@string/clear"
9 app:showAsAction="always" />
10
11</menu>