Merge pull request #12642 from t895/adapter-refactor
android: Refactor list adapters
This commit is contained in:
		@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class DiffCallback : DiffUtil.ItemCallback<Game>() {
 | 
			
		||||
        override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
 | 
			
		||||
            return oldItem == newItem
 | 
			
		||||
            binding.cardGame.setOnClickListener { onClick(model) }
 | 
			
		||||
            binding.cardGame.setOnLongClickListener { onLongClick(model) }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun areContentsTheSame(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)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun onLongClick(game: Game): Boolean {
 | 
			
		||||
            val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
 | 
			
		||||
            binding.root.findNavController().navigate(action)
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        AbstractViewHolder<Installable>(binding) {
 | 
			
		||||
        override fun bind(model: Installable) {
 | 
			
		||||
            binding.title.setText(model.titleId)
 | 
			
		||||
            binding.description.setText(model.descriptionId)
 | 
			
		||||
 | 
			
		||||
        fun bind(installable: Installable) {
 | 
			
		||||
            this.installable = installable
 | 
			
		||||
 | 
			
		||||
            binding.title.setText(installable.titleId)
 | 
			
		||||
            binding.description.setText(installable.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() }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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 ?: ""
 | 
			
		||||
            )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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)
 | 
			
		||||
        }
 | 
			
		||||
        updateDriverList()
 | 
			
		||||
        updateDriverNameForGame(null)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setSelectedDriverIndex(value: Int) {
 | 
			
		||||
        if (selectedDriver != -1) {
 | 
			
		||||
            previouslySelectedDriver = selectedDriver
 | 
			
		||||
    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))
 | 
			
		||||
        }
 | 
			
		||||
        selectedDriver = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setNewDriverInstalled(value: Boolean) {
 | 
			
		||||
        _newDriverInstalled.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)
 | 
			
		||||
        } else {
 | 
			
		||||
            setSelectedDriverIndex(driverIndex)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) {
 | 
			
		||||
        _driverList.value.remove(driverData)
 | 
			
		||||
        _driverList.value = newDriverList
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onOpenDriverManager(game: Game?) {
 | 
			
		||||
        if (game != null) {
 | 
			
		||||
            SettingsFile.loadCustomConfig(game)
 | 
			
		||||
        }
 | 
			
		||||
        updateDriverList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        val driverPath = StringSetting.DRIVER_PATH.getString()
 | 
			
		||||
        if (driverPath.isEmpty()) {
 | 
			
		||||
            setSelectedDriverIndex(0)
 | 
			
		||||
    fun showClearButton(value: Boolean) {
 | 
			
		||||
        _showClearButton.value = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onDriverSelected(position: Int) {
 | 
			
		||||
        if (position == 0) {
 | 
			
		||||
            StringSetting.DRIVER_PATH.setString("")
 | 
			
		||||
        } else {
 | 
			
		||||
            findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath)))
 | 
			
		||||
            StringSetting.DRIVER_PATH.setString(driverData[position - 1].first)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onDriverRemoved(removedPosition: Int, selectedPosition: Int) {
 | 
			
		||||
        driversToDelete.add(driverData[removedPosition - 1].first)
 | 
			
		||||
        driverData.removeAt(removedPosition - 1)
 | 
			
		||||
        onDriverSelected(selectedPosition)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/android/app/src/main/res/menu/menu_driver_manager.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/android/app/src/main/res/menu/menu_driver_manager.xml
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user