diff options
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 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder +import androidx.recyclerview.widget.RecyclerView + +/** + * Generic adapter that implements an [AsyncDifferConfig] and covers some of the basic boilerplate + * code used in every [RecyclerView]. + * Type assigned to [Model] must inherit from [Object] in order to be compared properly. + */ +abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>> : +    ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>()).build()) { +    override fun onBindViewHolder(holder: Holder, position: Int) = +        holder.bind(currentList[position]) + +    private class DiffCallback<Model> : DiffUtil.ItemCallback<Model>() { +        override fun areItemsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean { +            return oldItem === newItem +        } + +        @SuppressLint("DiffUtilEquals") +        override fun areContentsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean { +            return oldItem == newItem +        } +    } +} 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 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder + +/** + * Generic list class meant to take care of basic lists + * @param currentList The list to show initially + */ +abstract class AbstractListAdapter<Model : Any, Holder : AbstractViewHolder<Model>>( +    open var currentList: List<Model> +) : RecyclerView.Adapter<Holder>() { +    override fun onBindViewHolder(holder: Holder, position: Int) = +        holder.bind(currentList[position]) + +    override fun getItemCount(): Int = currentList.size + +    /** +     * Adds an item to [currentList] and notifies the underlying adapter of the change. If no parameter +     * is passed in for position, [item] is added to the end of the list. Invokes [callback] last. +     * @param item The item to add to the list +     * @param position Index where [item] will be added +     * @param callback Lambda that's called at the end of the list changes and has the added list +     * position passed in as a parameter +     */ +    open fun addItem(item: Model, position: Int = -1, callback: ((position: Int) -> Unit)? = null) { +        val newList = currentList.toMutableList() +        val positionToUpdate: Int +        if (position == -1) { +            newList.add(item) +            currentList = newList +            positionToUpdate = currentList.size - 1 +        } else { +            newList.add(position, item) +            currentList = newList +            positionToUpdate = position +        } +        onItemAdded(positionToUpdate, callback) +    } + +    protected fun onItemAdded(position: Int, callback: ((Int) -> Unit)? = null) { +        notifyItemInserted(position) +        callback?.invoke(position) +    } + +    /** +     * Replaces the [item] at [position] in the [currentList] and notifies the underlying adapter +     * of the change. Invokes [callback] last. +     * @param item New list item +     * @param position Index where [item] will replace the existing list item +     * @param callback Lambda that's called at the end of the list changes and has the changed list +     * position passed in as a parameter +     */ +    fun changeItem(item: Model, position: Int, callback: ((position: Int) -> Unit)? = null) { +        val newList = currentList.toMutableList() +        newList[position] = item +        currentList = newList +        onItemChanged(position, callback) +    } + +    protected fun onItemChanged(position: Int, callback: ((Int) -> Unit)? = null) { +        notifyItemChanged(position) +        callback?.invoke(position) +    } + +    /** +     * Removes the list item at [position] in [currentList] and notifies the underlying adapter +     * of the change. Invokes [callback] last. +     * @param position Index where the list item will be removed +     * @param callback Lambda that's called at the end of the list changes and has the removed list +     * position passed in as a parameter +     */ +    fun removeItem(position: Int, callback: ((position: Int) -> Unit)? = null) { +        val newList = currentList.toMutableList() +        newList.removeAt(position) +        currentList = newList +        onItemRemoved(position, callback) +    } + +    protected fun onItemRemoved(position: Int, callback: ((Int) -> Unit)? = null) { +        notifyItemRemoved(position) +        callback?.invoke(position) +    } + +    /** +     * Replaces [currentList] with [newList] and notifies the underlying adapter of the change. +     * @param newList The new list to replace [currentList] +     */ +    @SuppressLint("NotifyDataSetChanged") +    open fun replaceList(newList: List<Model>) { +        currentList = newList +        notifyDataSetChanged() +    } +} 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 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import org.yuzu.yuzu_emu.model.SelectableItem +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder + +/** + * Generic list class meant to take care of single selection UI updates + * @param currentList The list to show initially + * @param defaultSelection The default selection to use if no list items are selected by + * [SelectableItem.selected] or if the currently selected item is removed from the list + */ +abstract class AbstractSingleSelectionList< +    Model : SelectableItem, +    Holder : AbstractViewHolder<Model> +    >( +    final override var currentList: List<Model>, +    private val defaultSelection: DefaultSelection = DefaultSelection.Start +) : AbstractListAdapter<Model, Holder>(currentList) { +    var selectedItem = getDefaultSelection() + +    init { +        findSelectedItem() +    } + +    /** +     * Changes the selection state of the [SelectableItem] that was selected and the previously selected +     * item and notifies the underlying adapter of the change for those items. Invokes [callback] last. +     * Does nothing if [position] is the same as the currently selected item. +     * @param position Index of the item that was selected +     * @param callback Lambda that's called at the end of the list changes and has the selected list +     * position passed in as a parameter +     */ +    fun selectItem(position: Int, callback: ((position: Int) -> Unit)? = null) { +        if (position == selectedItem) { +            return +        } + +        val previouslySelectedItem = selectedItem +        selectedItem = position +        if (currentList.indices.contains(selectedItem)) { +            currentList[selectedItem].onSelectionStateChanged(true) +        } +        if (currentList.indices.contains(previouslySelectedItem)) { +            currentList[previouslySelectedItem].onSelectionStateChanged(false) +        } +        onItemChanged(previouslySelectedItem) +        onItemChanged(selectedItem) +        callback?.invoke(position) +    } + +    /** +     * Removes a given item from the list and notifies the underlying adapter of the change. If the +     * currently selected item was the item that was removed, the item at the position provided +     * by [defaultSelection] will be made the new selection. Invokes [callback] last. +     * @param position Index of the item that was removed +     * @param callback Lambda that's called at the end of the list changes and has the removed and +     * selected list positions passed in as parameters +     */ +    fun removeSelectableItem( +        position: Int, +        callback: ((removedPosition: Int, selectedPosition: Int) -> Unit)? +    ) { +        removeItem(position) +        if (position == selectedItem) { +            selectedItem = getDefaultSelection() +            currentList[selectedItem].onSelectionStateChanged(true) +            onItemChanged(selectedItem) +        } else if (position < selectedItem) { +            selectedItem-- +        } +        callback?.invoke(position, selectedItem) +    } + +    override fun addItem(item: Model, position: Int, callback: ((Int) -> Unit)?) { +        super.addItem(item, position, callback) +        if (position <= selectedItem && position != -1) { +            selectedItem++ +        } +    } + +    override fun replaceList(newList: List<Model>) { +        super.replaceList(newList) +        findSelectedItem() +    } + +    private fun findSelectedItem() { +        for (i in currentList.indices) { +            if (currentList[i].selected) { +                selectedItem = i +                break +            } +        } +    } + +    private fun getDefaultSelection(): Int = +        when (defaultSelection) { +            DefaultSelection.Start -> currentList.indices.first +            DefaultSelection.End -> currentList.indices.last +        } + +    enum class DefaultSelection { Start, End } +} 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  import android.view.LayoutInflater  import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView  import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding  import org.yuzu.yuzu_emu.model.Addon +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>( -    AsyncDifferConfig.Builder(DiffCallback()).build() -) { +class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {          ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)              .also { return AddonViewHolder(it) }      } -    override fun getItemCount(): Int = currentList.size - -    override fun onBindViewHolder(holder: AddonViewHolder, position: Int) = -        holder.bind(currentList[position]) -      inner class AddonViewHolder(val binding: ListItemAddonBinding) : -        RecyclerView.ViewHolder(binding.root) { -        fun bind(addon: Addon) { +        AbstractViewHolder<Addon>(binding) { +        override fun bind(model: Addon) {              binding.root.setOnClickListener {                  binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked              } -            binding.title.text = addon.title -            binding.version.text = addon.version +            binding.title.text = model.title +            binding.version.text = model.version              binding.addonSwitch.setOnCheckedChangeListener { _, checked -> -                addon.enabled = checked +                model.enabled = checked              } -            binding.addonSwitch.isChecked = addon.enabled -        } -    } - -    private class DiffCallback : DiffUtil.ItemCallback<Addon>() { -        override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean { -            return oldItem == newItem -        } - -        override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean { -            return oldItem == newItem +            binding.addonSwitch.isChecked = model.enabled          }      }  } 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 @@  package org.yuzu.yuzu_emu.adapters  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import android.widget.Toast  import androidx.core.content.res.ResourcesCompat  import androidx.fragment.app.FragmentActivity  import androidx.navigation.findNavController -import androidx.recyclerview.widget.RecyclerView  import org.yuzu.yuzu_emu.HomeNavigationDirections  import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R @@ -19,72 +17,58 @@ import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding  import org.yuzu.yuzu_emu.model.Applet  import org.yuzu.yuzu_emu.model.AppletInfo  import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) : -    RecyclerView.Adapter<AppletAdapter.AppletViewHolder>(), -    View.OnClickListener { - +class AppletAdapter(val activity: FragmentActivity, applets: List<Applet>) : +    AbstractListAdapter<Applet, AppletAdapter.AppletViewHolder>(applets) {      override fun onCreateViewHolder(          parent: ViewGroup,          viewType: Int      ): AppletAdapter.AppletViewHolder {          CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false) -            .apply { root.setOnClickListener(this@AppletAdapter) }              .also { return AppletViewHolder(it) }      } -    override fun onBindViewHolder(holder: AppletViewHolder, position: Int) = -        holder.bind(applets[position]) - -    override fun getItemCount(): Int = applets.size - -    override fun onClick(view: View) { -        val applet = (view.tag as AppletViewHolder).applet -        val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId) -        if (appletPath.isEmpty()) { -            Toast.makeText( -                YuzuApplication.appContext, -                R.string.applets_error_applet, -                Toast.LENGTH_SHORT -            ).show() -            return -        } - -        if (applet.appletInfo == AppletInfo.Cabinet) { -            view.findNavController() -                .navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment) -            return -        } - -        NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId) -        val appletGame = Game( -            title = YuzuApplication.appContext.getString(applet.titleId), -            path = appletPath -        ) -        val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) -        view.findNavController().navigate(action) -    } -      inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) : -        RecyclerView.ViewHolder(binding.root) { -        lateinit var applet: Applet - -        init { -            itemView.tag = this -        } - -        fun bind(applet: Applet) { -            this.applet = applet - -            binding.title.setText(applet.titleId) -            binding.description.setText(applet.descriptionId) +        AbstractViewHolder<Applet>(binding) { +        override fun bind(model: Applet) { +            binding.title.setText(model.titleId) +            binding.description.setText(model.descriptionId)              binding.icon.setImageDrawable(                  ResourcesCompat.getDrawable(                      binding.icon.context.resources, -                    applet.iconId, +                    model.iconId,                      binding.icon.context.theme                  )              ) + +            binding.root.setOnClickListener { onClick(model) } +        } + +        fun onClick(applet: Applet) { +            val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId) +            if (appletPath.isEmpty()) { +                Toast.makeText( +                    binding.root.context, +                    R.string.applets_error_applet, +                    Toast.LENGTH_SHORT +                ).show() +                return +            } + +            if (applet.appletInfo == AppletInfo.Cabinet) { +                binding.root.findNavController() +                    .navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment) +                return +            } + +            NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId) +            val appletGame = Game( +                title = YuzuApplication.appContext.getString(applet.titleId), +                path = appletPath +            ) +            val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) +            binding.root.findNavController().navigate(action)          }      }  } 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 @@  package org.yuzu.yuzu_emu.adapters  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import androidx.core.content.res.ResourcesCompat  import androidx.fragment.app.Fragment  import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.RecyclerView  import org.yuzu.yuzu_emu.HomeNavigationDirections  import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R @@ -19,54 +17,43 @@ import org.yuzu.yuzu_emu.model.CabinetMode  import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder  import org.yuzu.yuzu_emu.model.AppletInfo  import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class CabinetLauncherDialogAdapter(val fragment: Fragment) : -    RecyclerView.Adapter<CabinetModeViewHolder>(), -    View.OnClickListener { -    private val cabinetModes = CabinetMode.values().copyOfRange(1, CabinetMode.values().size) +    AbstractListAdapter<CabinetMode, CabinetModeViewHolder>( +        CabinetMode.values().copyOfRange(1, CabinetMode.entries.size).toList() +    ) {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder {          DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) -            .apply { root.setOnClickListener(this@CabinetLauncherDialogAdapter) }              .also { return CabinetModeViewHolder(it) }      } -    override fun getItemCount(): Int = cabinetModes.size - -    override fun onBindViewHolder(holder: CabinetModeViewHolder, position: Int) = -        holder.bind(cabinetModes[position]) - -    override fun onClick(view: View) { -        val mode = (view.tag as CabinetModeViewHolder).cabinetMode -        val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId) -        NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId) -        NativeLibrary.setCabinetMode(mode.id) -        val appletGame = Game( -            title = YuzuApplication.appContext.getString(R.string.cabinet_applet), -            path = appletPath -        ) -        val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) -        fragment.findNavController().navigate(action) -    } -      inner class CabinetModeViewHolder(val binding: DialogListItemBinding) : -        RecyclerView.ViewHolder(binding.root) { -        lateinit var cabinetMode: CabinetMode - -        init { -            itemView.tag = this -        } - -        fun bind(cabinetMode: CabinetMode) { -            this.cabinetMode = cabinetMode +        AbstractViewHolder<CabinetMode>(binding) { +        override fun bind(model: CabinetMode) {              binding.icon.setImageDrawable(                  ResourcesCompat.getDrawable(                      binding.icon.context.resources, -                    cabinetMode.iconId, +                    model.iconId,                      binding.icon.context.theme                  )              ) -            binding.title.setText(cabinetMode.titleId) +            binding.title.setText(model.titleId) + +            binding.root.setOnClickListener { onClick(model) } +        } + +        private fun onClick(mode: CabinetMode) { +            val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId) +            NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId) +            NativeLibrary.setCabinetMode(mode.id) +            val appletGame = Game( +                title = YuzuApplication.appContext.getString(R.string.cabinet_applet), +                path = appletPath +            ) +            val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) +            fragment.findNavController().navigate(action)          }      }  } 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  import android.view.LayoutInflater  import android.view.View  import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding +import org.yuzu.yuzu_emu.features.settings.model.StringSetting +import org.yuzu.yuzu_emu.model.Driver  import org.yuzu.yuzu_emu.model.DriverViewModel -import org.yuzu.yuzu_emu.utils.GpuDriverHelper -import org.yuzu.yuzu_emu.utils.GpuDriverMetadata +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class DriverAdapter(private val driverViewModel: DriverViewModel) : -    ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>( -        AsyncDifferConfig.Builder(DiffCallback()).build() +    AbstractSingleSelectionList<Driver, DriverAdapter.DriverViewHolder>( +        driverViewModel.driverList.value      ) {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder { -        val binding = -            CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) -        return DriverViewHolder(binding) -    } - -    override fun getItemCount(): Int = currentList.size - -    override fun onBindViewHolder(holder: DriverViewHolder, position: Int) = -        holder.bind(currentList[position]) - -    private fun onSelectDriver(position: Int) { -        driverViewModel.setSelectedDriverIndex(position) -        notifyItemChanged(driverViewModel.previouslySelectedDriver) -        notifyItemChanged(driverViewModel.selectedDriver) -    } - -    private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) { -        if (driverViewModel.selectedDriver > position) { -            driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1) -        } -        if (GpuDriverHelper.customDriverSettingData == driverData.second) { -            driverViewModel.setSelectedDriverIndex(0) -        } -        driverViewModel.driversToDelete.add(driverData.first) -        driverViewModel.removeDriver(driverData) -        notifyItemRemoved(position) -        notifyItemChanged(driverViewModel.selectedDriver) +        CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .also { return DriverViewHolder(it) }      }      inner class DriverViewHolder(val binding: CardDriverOptionBinding) : -        RecyclerView.ViewHolder(binding.root) { -        private lateinit var driverData: Pair<String, GpuDriverMetadata> - -        fun bind(driverData: Pair<String, GpuDriverMetadata>) { -            this.driverData = driverData -            val driver = driverData.second - +        AbstractViewHolder<Driver>(binding) { +        override fun bind(model: Driver) {              binding.apply { -                radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition +                radioButton.isChecked = model.selected                  root.setOnClickListener { -                    onSelectDriver(bindingAdapterPosition) +                    selectItem(bindingAdapterPosition) { +                        driverViewModel.onDriverSelected(it) +                        driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global) +                    }                  }                  buttonDelete.setOnClickListener { -                    onDeleteDriver(driverData, bindingAdapterPosition) +                    removeSelectableItem( +                        bindingAdapterPosition +                    ) { removedPosition: Int, selectedPosition: Int -> +                        driverViewModel.onDriverRemoved(removedPosition, selectedPosition) +                        driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global) +                    }                  }                  // Delay marquee by 3s @@ -80,38 +54,19 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :                      },                      3000                  ) -                if (driver.name == null) { -                    title.setText(R.string.system_gpu_driver) -                    description.text = "" -                    version.text = "" -                    version.visibility = View.GONE -                    description.visibility = View.GONE -                    buttonDelete.visibility = View.GONE -                } else { -                    title.text = driver.name -                    version.text = driver.version -                    description.text = driver.description +                title.text = model.title +                version.text = model.version +                description.text = model.description +                if (model.description.isNotEmpty()) {                      version.visibility = View.VISIBLE                      description.visibility = View.VISIBLE                      buttonDelete.visibility = View.VISIBLE +                } else { +                    version.visibility = View.GONE +                    description.visibility = View.GONE +                    buttonDelete.visibility = View.GONE                  }              }          }      } - -    private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() { -        override fun areItemsTheSame( -            oldItem: Pair<String, GpuDriverMetadata>, -            newItem: Pair<String, GpuDriverMetadata> -        ): Boolean { -            return oldItem.first == newItem.first -        } - -        override fun areContentsTheSame( -            oldItem: Pair<String, GpuDriverMetadata>, -            newItem: Pair<String, GpuDriverMetadata> -        ): Boolean { -            return oldItem.second == newItem.second -        } -    }  } 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  import android.view.LayoutInflater  import android.view.ViewGroup  import androidx.fragment.app.FragmentActivity -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView  import org.yuzu.yuzu_emu.databinding.CardFolderBinding  import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment  import org.yuzu.yuzu_emu.model.GameDir  import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : -    ListAdapter<GameDir, FolderAdapter.FolderViewHolder>( -        AsyncDifferConfig.Builder(DiffCallback()).build() -    ) { +    AbstractDiffAdapter<GameDir, FolderAdapter.FolderViewHolder>() {      override fun onCreateViewHolder(          parent: ViewGroup,          viewType: Int @@ -29,18 +24,11 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie              .also { return FolderViewHolder(it) }      } -    override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) = -        holder.bind(currentList[position]) -      inner class FolderViewHolder(val binding: CardFolderBinding) : -        RecyclerView.ViewHolder(binding.root) { -        private lateinit var gameDir: GameDir - -        fun bind(gameDir: GameDir) { -            this.gameDir = gameDir - +        AbstractViewHolder<GameDir>(binding) { +        override fun bind(model: GameDir) {              binding.apply { -                path.text = Uri.parse(gameDir.uriString).path +                path.text = Uri.parse(model.uriString).path                  path.postDelayed(                      {                          path.isSelected = true @@ -50,7 +38,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie                  )                  buttonEdit.setOnClickListener { -                    GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir) +                    GameFolderPropertiesDialogFragment.newInstance(model)                          .show(                              activity.supportFragmentManager,                              GameFolderPropertiesDialogFragment.TAG @@ -58,19 +46,9 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie                  }                  buttonDelete.setOnClickListener { -                    gamesViewModel.removeFolder(this@FolderViewHolder.gameDir) +                    gamesViewModel.removeFolder(model)                  }              }          }      } - -    private class DiffCallback : DiffUtil.ItemCallback<GameDir>() { -        override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean { -            return oldItem == newItem -        } - -        override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean { -            return oldItem == newItem -        } -    }  } 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  import android.net.Uri  import android.text.TextUtils  import android.view.LayoutInflater -import android.view.View  import android.view.ViewGroup  import android.widget.ImageView  import android.widget.Toast @@ -25,10 +24,6 @@ import androidx.lifecycle.ViewModelProvider  import androidx.lifecycle.lifecycleScope  import androidx.navigation.findNavController  import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView  import kotlinx.coroutines.Dispatchers  import kotlinx.coroutines.launch  import kotlinx.coroutines.withContext @@ -36,122 +31,26 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.YuzuApplication  import org.yuzu.yuzu_emu.activities.EmulationActivity -import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder  import org.yuzu.yuzu_emu.databinding.CardGameBinding  import org.yuzu.yuzu_emu.model.Game  import org.yuzu.yuzu_emu.model.GamesViewModel  import org.yuzu.yuzu_emu.utils.GameIconUtils +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class GameAdapter(private val activity: AppCompatActivity) : -    ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), -    View.OnClickListener, -    View.OnLongClickListener { +    AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>() {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { -        // Create a new view. -        val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) -        binding.cardGame.setOnClickListener(this) -        binding.cardGame.setOnLongClickListener(this) - -        // Use that view to create a ViewHolder. -        return GameViewHolder(binding) -    } - -    override fun onBindViewHolder(holder: GameViewHolder, position: Int) = -        holder.bind(currentList[position]) - -    override fun getItemCount(): Int = currentList.size - -    /** -     * Launches the game that was clicked on. -     * -     * @param view The card representing the game the user wants to play. -     */ -    override fun onClick(view: View) { -        val holder = view.tag as GameViewHolder - -        val gameExists = DocumentFile.fromSingleUri( -            YuzuApplication.appContext, -            Uri.parse(holder.game.path) -        )?.exists() == true -        if (!gameExists) { -            Toast.makeText( -                YuzuApplication.appContext, -                R.string.loader_error_file_not_found, -                Toast.LENGTH_LONG -            ).show() - -            ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) -            return -        } - -        val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) -        preferences.edit() -            .putLong( -                holder.game.keyLastPlayedTime, -                System.currentTimeMillis() -            ) -            .apply() - -        val openIntent = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply { -            action = Intent.ACTION_VIEW -            data = Uri.parse(holder.game.path) -        } - -        activity.lifecycleScope.launch { -            withContext(Dispatchers.IO) { -                val layerDrawable = ResourcesCompat.getDrawable( -                    YuzuApplication.appContext.resources, -                    R.drawable.shortcut, -                    null -                ) as LayerDrawable -                layerDrawable.setDrawableByLayerId( -                    R.id.shortcut_foreground, -                    GameIconUtils.getGameIcon(activity, holder.game) -                        .toDrawable(YuzuApplication.appContext.resources) -                ) -                val inset = YuzuApplication.appContext.resources -                    .getDimensionPixelSize(R.dimen.icon_inset) -                layerDrawable.setLayerInset(1, inset, inset, inset, inset) -                val shortcut = -                    ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path) -                        .setShortLabel(holder.game.title) -                        .setIcon( -                            IconCompat.createWithAdaptiveBitmap( -                                layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888) -                            ) -                        ) -                        .setIntent(openIntent) -                        .build() -                ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut) -            } -        } - -        val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true) -        view.findNavController().navigate(action) -    } - -    override fun onLongClick(view: View): Boolean { -        val holder = view.tag as GameViewHolder -        val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game) -        view.findNavController().navigate(action) -        return true +        CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .also { return GameViewHolder(it) }      }      inner class GameViewHolder(val binding: CardGameBinding) : -        RecyclerView.ViewHolder(binding.root) { -        lateinit var game: Game - -        init { -            binding.cardGame.tag = this -        } - -        fun bind(game: Game) { -            this.game = game - +        AbstractViewHolder<Game>(binding) { +        override fun bind(model: Game) {              binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP -            GameIconUtils.loadGameIcon(game, binding.imageGameScreen) +            GameIconUtils.loadGameIcon(model, binding.imageGameScreen) -            binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") +            binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")              binding.textGameTitle.postDelayed(                  { @@ -160,16 +59,79 @@ class GameAdapter(private val activity: AppCompatActivity) :                  },                  3000              ) + +            binding.cardGame.setOnClickListener { onClick(model) } +            binding.cardGame.setOnLongClickListener { onLongClick(model) }          } -    } -    private class DiffCallback : DiffUtil.ItemCallback<Game>() { -        override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { -            return oldItem == newItem +        fun onClick(game: Game) { +            val gameExists = DocumentFile.fromSingleUri( +                YuzuApplication.appContext, +                Uri.parse(game.path) +            )?.exists() == true +            if (!gameExists) { +                Toast.makeText( +                    YuzuApplication.appContext, +                    R.string.loader_error_file_not_found, +                    Toast.LENGTH_LONG +                ).show() + +                ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) +                return +            } + +            val preferences = +                PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) +            preferences.edit() +                .putLong( +                    game.keyLastPlayedTime, +                    System.currentTimeMillis() +                ) +                .apply() + +            val openIntent = +                Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply { +                    action = Intent.ACTION_VIEW +                    data = Uri.parse(game.path) +                } + +            activity.lifecycleScope.launch { +                withContext(Dispatchers.IO) { +                    val layerDrawable = ResourcesCompat.getDrawable( +                        YuzuApplication.appContext.resources, +                        R.drawable.shortcut, +                        null +                    ) as LayerDrawable +                    layerDrawable.setDrawableByLayerId( +                        R.id.shortcut_foreground, +                        GameIconUtils.getGameIcon(activity, game) +                            .toDrawable(YuzuApplication.appContext.resources) +                    ) +                    val inset = YuzuApplication.appContext.resources +                        .getDimensionPixelSize(R.dimen.icon_inset) +                    layerDrawable.setLayerInset(1, inset, inset, inset, inset) +                    val shortcut = +                        ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path) +                            .setShortLabel(game.title) +                            .setIcon( +                                IconCompat.createWithAdaptiveBitmap( +                                    layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888) +                                ) +                            ) +                            .setIntent(openIntent) +                            .build() +                    ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut) +                } +            } + +            val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) +            binding.root.findNavController().navigate(action)          } -        override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { -            return oldItem == newItem +        fun onLongClick(game: Game): Boolean { +            val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game) +            binding.root.findNavController().navigate(action) +            return true          }      }  } 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  import androidx.lifecycle.LifecycleOwner  import androidx.lifecycle.lifecycleScope  import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.RecyclerView  import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding  import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding  import org.yuzu.yuzu_emu.model.GameProperty  import org.yuzu.yuzu_emu.model.InstallableProperty  import org.yuzu.yuzu_emu.model.SubmenuProperty +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class GamePropertiesAdapter(      private val viewLifecycle: LifecycleOwner,      private var properties: List<GameProperty> -) : -    RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() { +) : AbstractListAdapter<GameProperty, AbstractViewHolder<GameProperty>>(properties) {      override fun onCreateViewHolder(          parent: ViewGroup,          viewType: Int -    ): GamePropertyViewHolder { +    ): AbstractViewHolder<GameProperty> {          val inflater = LayoutInflater.from(parent.context)          return when (viewType) {              PropertyType.Submenu.ordinal -> { @@ -51,11 +50,6 @@ class GamePropertiesAdapter(          }      } -    override fun getItemCount(): Int = properties.size - -    override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) = -        holder.bind(properties[position]) -      override fun getItemViewType(position: Int): Int {          return when (properties[position]) {              is SubmenuProperty -> PropertyType.Submenu.ordinal @@ -63,14 +57,10 @@ class GamePropertiesAdapter(          }      } -    sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { -        abstract fun bind(property: GameProperty) -    } -      inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) : -        GamePropertyViewHolder(binding.root) { -        override fun bind(property: GameProperty) { -            val submenuProperty = property as SubmenuProperty +        AbstractViewHolder<GameProperty>(binding) { +        override fun bind(model: GameProperty) { +            val submenuProperty = model as SubmenuProperty              binding.root.setOnClickListener {                  submenuProperty.action.invoke() @@ -108,9 +98,9 @@ class GamePropertiesAdapter(      }      inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) : -        GamePropertyViewHolder(binding.root) { -        override fun bind(property: GameProperty) { -            val installableProperty = property as InstallableProperty +        AbstractViewHolder<GameProperty>(binding) { +        override fun bind(model: GameProperty) { +            val installableProperty = model as InstallableProperty              binding.title.setText(installableProperty.titleId)              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  import androidx.lifecycle.LifecycleOwner  import androidx.lifecycle.lifecycleScope  import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.RecyclerView  import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding  import org.yuzu.yuzu_emu.fragments.MessageDialogFragment  import org.yuzu.yuzu_emu.model.HomeSetting +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder  class HomeSettingAdapter(      private val activity: AppCompatActivity,      private val viewLifecycle: LifecycleOwner, -    var options: List<HomeSetting> -) : -    RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(), -    View.OnClickListener { +    options: List<HomeSetting> +) : AbstractListAdapter<HomeSetting, HomeSettingAdapter.HomeOptionViewHolder>(options) {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { -        val binding = -            CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) -        binding.root.setOnClickListener(this) -        return HomeOptionViewHolder(binding) -    } - -    override fun getItemCount(): Int { -        return options.size -    } - -    override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) { -        holder.bind(options[position]) -    } - -    override fun onClick(view: View) { -        val holder = view.tag as HomeOptionViewHolder -        if (holder.option.isEnabled.invoke()) { -            holder.option.onClick.invoke() -        } else { -            MessageDialogFragment.newInstance( -                activity, -                titleId = holder.option.disabledTitleId, -                descriptionId = holder.option.disabledMessageId -            ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) -        } +        CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .also { return HomeOptionViewHolder(it) }      }      inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) : -        RecyclerView.ViewHolder(binding.root) { -        lateinit var option: HomeSetting - -        init { -            itemView.tag = this -        } - -        fun bind(option: HomeSetting) { -            this.option = option -            binding.optionTitle.text = activity.resources.getString(option.titleId) -            binding.optionDescription.text = activity.resources.getString(option.descriptionId) +        AbstractViewHolder<HomeSetting>(binding) { +        override fun bind(model: HomeSetting) { +            binding.optionTitle.text = activity.resources.getString(model.titleId) +            binding.optionDescription.text = activity.resources.getString(model.descriptionId)              binding.optionIcon.setImageDrawable(                  ResourcesCompat.getDrawable(                      activity.resources, -                    option.iconId, +                    model.iconId,                      activity.theme                  )              ) -            when (option.titleId) { +            when (model.titleId) {                  R.string.get_early_access ->                      binding.optionLayout.background =                          ContextCompat.getDrawable( @@ -85,7 +53,7 @@ class HomeSettingAdapter(                          )              } -            if (!option.isEnabled.invoke()) { +            if (!model.isEnabled.invoke()) {                  binding.optionTitle.alpha = 0.5f                  binding.optionDescription.alpha = 0.5f                  binding.optionIcon.alpha = 0.5f @@ -93,7 +61,7 @@ class HomeSettingAdapter(              viewLifecycle.lifecycleScope.launch {                  viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { -                    option.details.collect { updateOptionDetails(it) } +                    model.details.collect { updateOptionDetails(it) }                  }              }              binding.optionDetail.postDelayed( @@ -103,6 +71,20 @@ class HomeSettingAdapter(                  },                  3000              ) + +            binding.root.setOnClickListener { onClick(model) } +        } + +        private fun onClick(model: HomeSetting) { +            if (model.isEnabled.invoke()) { +                model.onClick.invoke() +            } else { +                MessageDialogFragment.newInstance( +                    activity, +                    titleId = model.disabledTitleId, +                    descriptionId = model.disabledMessageId +                ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) +            }          }          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  import android.view.LayoutInflater  import android.view.View  import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView  import org.yuzu.yuzu_emu.databinding.CardInstallableBinding  import org.yuzu.yuzu_emu.model.Installable +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class InstallableAdapter(private val installables: List<Installable>) : -    RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { +class InstallableAdapter(installables: List<Installable>) : +    AbstractListAdapter<Installable, InstallableAdapter.InstallableViewHolder>(installables) {      override fun onCreateViewHolder(          parent: ViewGroup,          viewType: Int      ): InstallableAdapter.InstallableViewHolder { -        val binding = -            CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) -        return InstallableViewHolder(binding) +        CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .also { return InstallableViewHolder(it) }      } -    override fun getItemCount(): Int = installables.size - -    override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) = -        holder.bind(installables[position]) -      inner class InstallableViewHolder(val binding: CardInstallableBinding) : -        RecyclerView.ViewHolder(binding.root) { -        lateinit var installable: Installable - -        fun bind(installable: Installable) { -            this.installable = installable - -            binding.title.setText(installable.titleId) -            binding.description.setText(installable.descriptionId) +        AbstractViewHolder<Installable>(binding) { +        override fun bind(model: Installable) { +            binding.title.setText(model.titleId) +            binding.description.setText(model.descriptionId) -            if (installable.install != null) { +            if (model.install != null) {                  binding.buttonInstall.visibility = View.VISIBLE -                binding.buttonInstall.setOnClickListener { installable.install.invoke() } +                binding.buttonInstall.setOnClickListener { model.install.invoke() }              } -            if (installable.export != null) { +            if (model.export != null) {                  binding.buttonExport.visibility = View.VISIBLE -                binding.buttonExport.setOnClickListener { installable.export.invoke() } +                binding.buttonExport.setOnClickListener { model.export.invoke() }              }          }      } 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  import android.view.View  import android.view.ViewGroup  import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import org.yuzu.yuzu_emu.YuzuApplication  import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding  import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment  import org.yuzu.yuzu_emu.model.License +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) : -    RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(), -    View.OnClickListener { +class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) : +    AbstractListAdapter<License, LicenseAdapter.LicenseViewHolder>(licenses) {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder { -        val binding = -            ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false) -        binding.root.setOnClickListener(this) -        return LicenseViewHolder(binding) +        ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .also { return LicenseViewHolder(it) }      } -    override fun getItemCount(): Int = licenses.size +    inner class LicenseViewHolder(val binding: ListItemSettingBinding) : +        AbstractViewHolder<License>(binding) { +        override fun bind(model: License) { +            binding.apply { +                textSettingName.text = root.context.getString(model.titleId) +                textSettingDescription.text = root.context.getString(model.descriptionId) +                textSettingValue.visibility = View.GONE -    override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) { -        holder.bind(licenses[position]) -    } - -    override fun onClick(view: View) { -        val license = (view.tag as LicenseViewHolder).license -        LicenseBottomSheetDialogFragment.newInstance(license) -            .show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG) -    } - -    inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) { -        lateinit var license: License - -        init { -            itemView.tag = this +                root.setOnClickListener { onClick(model) } +            }          } -        fun bind(license: License) { -            this.license = license - -            val context = YuzuApplication.appContext -            binding.textSettingName.text = context.getString(license.titleId) -            binding.textSettingDescription.text = context.getString(license.descriptionId) -            binding.textSettingValue.visibility = View.GONE +        private fun onClick(license: License) { +            LicenseBottomSheetDialogFragment.newInstance(license) +                .show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)          }      }  } 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  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 @@ -18,31 +17,19 @@ 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 +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) : -    RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() { +class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : +    AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder { -        val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false) -        return SetupPageViewHolder(binding) +        PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false) +            .also { return SetupPageViewHolder(it) }      } -    override fun getItemCount(): Int = pages.size - -    override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) = -        holder.bind(pages[position]) -      inner class SetupPageViewHolder(val binding: PageSetupBinding) : -        RecyclerView.ViewHolder(binding.root), SetupCallback { -        lateinit var page: SetupPage - -        init { -            itemView.tag = this -        } - -        fun bind(page: SetupPage) { -            this.page = page - -            if (page.stepCompleted.invoke() == StepState.COMPLETE) { +        AbstractViewHolder<SetupPage>(binding), SetupCallback { +        override fun bind(model: SetupPage) { +            if (model.stepCompleted.invoke() == StepState.COMPLETE) {                  binding.buttonAction.visibility = View.INVISIBLE                  binding.textConfirmation.visibility = View.VISIBLE              } @@ -50,31 +37,31 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)              binding.icon.setImageDrawable(                  ResourcesCompat.getDrawable(                      activity.resources, -                    page.iconId, +                    model.iconId,                      activity.theme                  )              ) -            binding.textTitle.text = activity.resources.getString(page.titleId) +            binding.textTitle.text = activity.resources.getString(model.titleId)              binding.textDescription.text = -                Html.fromHtml(activity.resources.getString(page.descriptionId), 0) +                Html.fromHtml(activity.resources.getString(model.descriptionId), 0)              binding.buttonAction.apply { -                text = activity.resources.getString(page.buttonTextId) -                if (page.buttonIconId != 0) { +                text = activity.resources.getString(model.buttonTextId) +                if (model.buttonIconId != 0) {                      icon = ResourcesCompat.getDrawable(                          activity.resources, -                        page.buttonIconId, +                        model.buttonIconId,                          activity.theme                      )                  }                  iconGravity = -                    if (page.leftAlignedIcon) { +                    if (model.leftAlignedIcon) {                          MaterialButton.ICON_GRAVITY_START                      } else {                          MaterialButton.ICON_GRAVITY_END                      }                  setOnClickListener { -                    page.buttonAction.invoke(this@SetupPageViewHolder) +                    model.buttonAction.invoke(this@SetupPageViewHolder)                  }              }          } 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 @@  package org.yuzu.yuzu_emu.fragments +import android.annotation.SuppressLint  import android.os.Bundle  import android.view.LayoutInflater  import android.view.View @@ -13,20 +14,26 @@ import androidx.core.view.WindowInsetsCompat  import androidx.core.view.updatePadding  import androidx.fragment.app.Fragment  import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle  import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.findNavController  import androidx.navigation.fragment.navArgs  import androidx.recyclerview.widget.GridLayoutManager  import com.google.android.material.transition.MaterialSharedAxis -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.Dispatchers  import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.adapters.DriverAdapter  import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding +import org.yuzu.yuzu_emu.features.settings.model.StringSetting +import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver  import org.yuzu.yuzu_emu.model.DriverViewModel  import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.utils.FileUtil  import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.utils.NativeConfig  import java.io.File  import java.io.IOException @@ -55,12 +62,43 @@ class DriverManagerFragment : Fragment() {          return binding.root      } +    // This is using the correct scope, lint is just acting up +    @SuppressLint("UnsafeRepeatOnLifecycleDetector")      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {          super.onViewCreated(view, savedInstanceState)          homeViewModel.setNavigationVisibility(visible = false, animated = true)          homeViewModel.setStatusBarShadeVisibility(visible = false)          driverViewModel.onOpenDriverManager(args.game) +        if (NativeConfig.isPerGameConfigLoaded()) { +            binding.toolbarDrivers.inflateMenu(R.menu.menu_driver_manager) +            driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global) +            binding.toolbarDrivers.setOnMenuItemClickListener { +                when (it.itemId) { +                    R.id.menu_driver_clear -> { +                        StringSetting.DRIVER_PATH.global = true +                        driverViewModel.updateDriverList() +                        (binding.listDrivers.adapter as DriverAdapter) +                            .replaceList(driverViewModel.driverList.value) +                        driverViewModel.showClearButton(false) +                        true +                    } + +                    else -> false +                } +            } + +            viewLifecycleOwner.lifecycleScope.apply { +                launch { +                    repeatOnLifecycle(Lifecycle.State.STARTED) { +                        driverViewModel.showClearButton.collect { +                            binding.toolbarDrivers.menu +                                .findItem(R.id.menu_driver_clear).isVisible = it +                        } +                    } +                } +            } +        }          if (!driverViewModel.isInteractionAllowed.value) {              DriversLoadingDialogFragment().show( @@ -85,25 +123,6 @@ class DriverManagerFragment : Fragment() {              adapter = DriverAdapter(driverViewModel)          } -        viewLifecycleOwner.lifecycleScope.apply { -            launch { -                driverViewModel.driverList.collectLatest { -                    (binding.listDrivers.adapter as DriverAdapter).submitList(it) -                } -            } -            launch { -                driverViewModel.newDriverInstalled.collect { -                    if (_binding != null && it) { -                        (binding.listDrivers.adapter as DriverAdapter).apply { -                            notifyItemChanged(driverViewModel.previouslySelectedDriver) -                            notifyItemChanged(driverViewModel.selectedDriver) -                            driverViewModel.setNewDriverInstalled(false) -                        } -                    } -                } -            } -        } -          setInsets()      } @@ -160,7 +179,7 @@ class DriverManagerFragment : Fragment() {                  false              ) {                  val driverPath = -                    "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}" +                    "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"                  val driverFile = File(driverPath)                  // Ignore file exceptions when a user selects an invalid zip @@ -177,12 +196,21 @@ class DriverManagerFragment : Fragment() {                  val driverData = GpuDriverHelper.getMetadataFromZip(driverFile)                  val driverInList = -                    driverViewModel.driverList.value.firstOrNull { it.second == driverData } +                    driverViewModel.driverData.firstOrNull { it.second == driverData }                  if (driverInList != null) {                      return@newInstance getString(R.string.driver_already_installed)                  } else { -                    driverViewModel.addDriver(Pair(driverPath, driverData)) -                    driverViewModel.setNewDriverInstalled(true) +                    driverViewModel.onDriverAdded(Pair(driverPath, driverData)) +                    withContext(Dispatchers.Main) { +                        if (_binding != null) { +                            val adapter = binding.listDrivers.adapter as DriverAdapter +                            adapter.addItem(driverData.toDriver()) +                            adapter.selectItem(adapter.currentList.indices.last) +                            driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global) +                            binding.listDrivers +                                .smoothScrollToPosition(adapter.currentList.indices.last) +                        } +                    }                  }                  return@newInstance Any()              }.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 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import org.yuzu.yuzu_emu.utils.GpuDriverMetadata + +data class Driver( +    override var selected: Boolean, +    val title: String, +    val version: String = "", +    val description: String = "" +) : SelectableItem { +    override fun onSelectionStateChanged(selected: Boolean) { +        this.selected = selected +    } + +    companion object { +        fun GpuDriverMetadata.toDriver(selected: Boolean = false): Driver = +            Driver( +                selected, +                this.name ?: "", +                this.version ?: "", +                this.description ?: "" +            ) +    } +} 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  import kotlinx.coroutines.flow.MutableStateFlow  import kotlinx.coroutines.flow.SharingStarted  import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow  import kotlinx.coroutines.flow.combine  import kotlinx.coroutines.flow.stateIn  import kotlinx.coroutines.launch @@ -17,11 +18,10 @@ import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.YuzuApplication  import org.yuzu.yuzu_emu.features.settings.model.StringSetting  import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile -import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver  import org.yuzu.yuzu_emu.utils.GpuDriverHelper  import org.yuzu.yuzu_emu.utils.GpuDriverMetadata  import org.yuzu.yuzu_emu.utils.NativeConfig -import java.io.BufferedOutputStream  import java.io.File  class DriverViewModel : ViewModel() { @@ -38,97 +38,81 @@ class DriverViewModel : ViewModel() {              !loading && ready && !deleting          }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false) -    private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers()) -    val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList +    var driverData = GpuDriverHelper.getDrivers() -    var previouslySelectedDriver = 0 -    var selectedDriver = -1 +    private val _driverList = MutableStateFlow(emptyList<Driver>()) +    val driverList: StateFlow<List<Driver>> get() = _driverList      // Used for showing which driver is currently installed within the driver manager card      private val _selectedDriverTitle = MutableStateFlow("")      val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle -    private val _newDriverInstalled = MutableStateFlow(false) -    val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled +    private val _showClearButton = MutableStateFlow(false) +    val showClearButton = _showClearButton.asStateFlow() -    val driversToDelete = mutableListOf<String>() +    private val driversToDelete = mutableListOf<String>()      init { -        val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData -        findSelectedDriver(currentDriverMetadata) - -        // If a user had installed a driver before the manager was implemented, this zips -        // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can -        // be indexed and exported as expected. -        if (selectedDriver == -1) { -            val driverToSave = -                File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip") -            driverToSave.createNewFile() -            FileUtil.zipFromInternalStorage( -                File(GpuDriverHelper.driverInstallationPath!!), -                GpuDriverHelper.driverInstallationPath!!, -                BufferedOutputStream(driverToSave.outputStream()) -            ) -            _driverList.value.add(Pair(driverToSave.path, currentDriverMetadata)) -            setSelectedDriverIndex(_driverList.value.size - 1) -        } +        updateDriverList() +        updateDriverNameForGame(null) +    } -        // If a user had installed a driver before the config was reworked to be multiplatform, -        // we have save the path of the previously selected driver to the new setting. -        if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 && -            StringSetting.DRIVER_PATH.global -        ) { -            StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first) -            NativeConfig.saveGlobalConfig() -        } else { -            findSelectedDriver(GpuDriverHelper.customDriverSettingData) +    fun reloadDriverData() { +        _areDriversLoading.value = true +        driverData = GpuDriverHelper.getDrivers() +        updateDriverList() +        _areDriversLoading.value = false +    } + +    fun updateDriverList() { +        val selectedDriver = GpuDriverHelper.customDriverSettingData +        val newDriverList = mutableListOf( +            Driver( +                selectedDriver == GpuDriverMetadata(), +                YuzuApplication.appContext.getString(R.string.system_gpu_driver) +            ) +        ) +        driverData.forEach { +            newDriverList.add(it.second.toDriver(it.second == selectedDriver))          } -        updateDriverNameForGame(null) +        _driverList.value = newDriverList      } -    fun setSelectedDriverIndex(value: Int) { -        if (selectedDriver != -1) { -            previouslySelectedDriver = selectedDriver +    fun onOpenDriverManager(game: Game?) { +        if (game != null) { +            SettingsFile.loadCustomConfig(game)          } -        selectedDriver = value +        updateDriverList()      } -    fun setNewDriverInstalled(value: Boolean) { -        _newDriverInstalled.value = value +    fun showClearButton(value: Boolean) { +        _showClearButton.value = value      } -    fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { -        val driverIndex = _driverList.value.indexOfFirst { it == driverData } -        if (driverIndex == -1) { -            _driverList.value.add(driverData) -            setSelectedDriverIndex(_driverList.value.size - 1) -            _selectedDriverTitle.value = driverData.second.name -                ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) +    fun onDriverSelected(position: Int) { +        if (position == 0) { +            StringSetting.DRIVER_PATH.setString("")          } else { -            setSelectedDriverIndex(driverIndex) +            StringSetting.DRIVER_PATH.setString(driverData[position - 1].first)          }      } -    fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) { -        _driverList.value.remove(driverData) +    fun onDriverRemoved(removedPosition: Int, selectedPosition: Int) { +        driversToDelete.add(driverData[removedPosition - 1].first) +        driverData.removeAt(removedPosition - 1) +        onDriverSelected(selectedPosition)      } -    fun onOpenDriverManager(game: Game?) { -        if (game != null) { -            SettingsFile.loadCustomConfig(game) -        } - -        val driverPath = StringSetting.DRIVER_PATH.getString() -        if (driverPath.isEmpty()) { -            setSelectedDriverIndex(0) -        } else { -            findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath))) +    fun onDriverAdded(driver: Pair<String, GpuDriverMetadata>) { +        if (driversToDelete.contains(driver.first)) { +            driversToDelete.remove(driver.first)          } +        driverData.add(driver) +        onDriverSelected(driverData.size)      }      fun onCloseDriverManager(game: Game?) {          _isDeletingDrivers.value = true -        StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)          updateDriverNameForGame(game)          if (game == null) {              NativeConfig.saveGlobalConfig() @@ -181,20 +165,6 @@ class DriverViewModel : ViewModel() {          }      } -    private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) { -        if (driverList.value.size == 1) { -            setSelectedDriverIndex(0) -            return -        } - -        driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> -> -            if (driver.second == currentDriverMetadata) { -                setSelectedDriverIndex(i) -                return -            } -        } -    } -      fun updateDriverNameForGame(game: Game?) {          if (!GpuDriverHelper.supportsCustomDriverLoading()) {              return @@ -217,7 +187,6 @@ class DriverViewModel : ViewModel() {      private fun setDriverReady() {          _isDriverReady.value = true -        _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name -            ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) +        updateName()      }  } 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 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +interface SelectableItem { +    var selected: Boolean +    fun onSelectionStateChanged(selected: Boolean) +} 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  import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment  import org.yuzu.yuzu_emu.fragments.MessageDialogFragment  import org.yuzu.yuzu_emu.model.AddonViewModel +import org.yuzu.yuzu_emu.model.DriverViewModel  import org.yuzu.yuzu_emu.model.GamesViewModel  import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.model.TaskState @@ -58,6 +59,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {      private val gamesViewModel: GamesViewModel by viewModels()      private val taskViewModel: TaskViewModel by viewModels()      private val addonViewModel: AddonViewModel by viewModels() +    private val driverViewModel: DriverViewModel by viewModels()      override var themeId: Int = 0 @@ -689,6 +691,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {                  NativeLibrary.initializeSystem(true)                  NativeConfig.initializeGlobalConfig()                  gamesViewModel.reloadGames(false) +                driverViewModel.reloadDriverData()                  return@newInstance getString(R.string.user_data_import_success)              }.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 {                  ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }                  ?.distinct()                  ?.toMutableList() ?: mutableListOf() - -        // TODO: Get system driver information -        drivers.add(0, Pair("", GpuDriverMetadata()))          return drivers      } 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 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.viewholder + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import org.yuzu.yuzu_emu.adapters.AbstractDiffAdapter +import org.yuzu.yuzu_emu.adapters.AbstractListAdapter + +/** + * [RecyclerView.ViewHolder] meant to work together with a [AbstractDiffAdapter] or a + * [AbstractListAdapter] so we can run [bind] on each list item without needing a manual hookup. + */ +abstract class AbstractViewHolder<Model>(binding: ViewBinding) : +    RecyclerView.ViewHolder(binding.root) { +    abstract fun bind(model: Model) +} 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto"> + +    <item +        android:id="@+id/menu_driver_clear" +        android:icon="@drawable/ic_clear" +        android:title="@string/clear" +        app:showAsAction="always" /> + +</menu> | 
