diff options
Diffstat (limited to 'src/android')
31 files changed, 708 insertions, 662 deletions
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 53aafa08c..06e59d1ac 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -188,8 +188,15 @@ tasks.create<Delete>("ktlintReset") { delete(File(buildDir.path + File.separator + "intermediates/ktLint")) } +val showFormatHelp = { + logger.lifecycle( + "If this check fails, please try running \"gradlew ktlintFormat\" for automatic " + + "codestyle fixes" + ) +} +tasks.getByPath("ktlintKotlinScriptCheck").doFirst { showFormatHelp.invoke() } +tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() } tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset") -tasks.getByPath("preBuild").dependsOn("ktlintCheck") ktlint { version.set("0.47.1") @@ -228,71 +235,33 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") } -fun getGitVersion(): String { - var versionName = "0.0" - - try { - versionName = ProcessBuilder("git", "describe", "--always", "--long") +fun runGitCommand(command: List<String>): String { + return try { + ProcessBuilder(command) .directory(project.rootDir) .redirectOutput(ProcessBuilder.Redirect.PIPE) .redirectError(ProcessBuilder.Redirect.PIPE) .start().inputStream.bufferedReader().use { it.readText() } .trim() - .replace(Regex("(-0)?-[^-]+$"), "") } catch (e: Exception) { - logger.error("Cannot find git, defaulting to dummy version number") + logger.error("Cannot find git") + "" } - - if (System.getenv("GITHUB_ACTIONS") != null) { - val gitTag = System.getenv("GIT_TAG_NAME") - versionName = gitTag ?: versionName - } - - return versionName } -fun getGitHash(): String { - try { - val processBuilder = ProcessBuilder("git", "rev-parse", "--short", "HEAD") - processBuilder.directory(project.rootDir) - val process = processBuilder.start() - val inputStream = process.inputStream - val errorStream = process.errorStream - process.waitFor() - - return if (process.exitValue() == 0) { - inputStream.bufferedReader() - .use { it.readText().trim() } // return the value of gitHash - } else { - val errorMessage = errorStream.bufferedReader().use { it.readText().trim() } - logger.error("Error running git command: $errorMessage") - "dummy-hash" // return a dummy hash value in case of an error - } - } catch (e: Exception) { - logger.error("$e: Cannot find git, defaulting to dummy build hash") - return "dummy-hash" // return a dummy hash value in case of an error +fun getGitVersion(): String { + val versionName = if (System.getenv("GITHUB_ACTIONS") != null) { + val gitTag = System.getenv("GIT_TAG_NAME") ?: "" + gitTag + } else { + runGitCommand(listOf("git", "describe", "--always", "--long")) + .replace(Regex("(-0)?-[^-]+$"), "") } + return versionName.ifEmpty { "0.0" } } -fun getBranch(): String { - try { - val processBuilder = ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD") - processBuilder.directory(project.rootDir) - val process = processBuilder.start() - val inputStream = process.inputStream - val errorStream = process.errorStream - process.waitFor() - - return if (process.exitValue() == 0) { - inputStream.bufferedReader() - .use { it.readText().trim() } // return the value of gitHash - } else { - val errorMessage = errorStream.bufferedReader().use { it.readText().trim() } - logger.error("Error running git command: $errorMessage") - "dummy-hash" // return a dummy hash value in case of an error - } - } catch (e: Exception) { - logger.error("$e: Cannot find git, defaulting to dummy build hash") - return "dummy-hash" // return a dummy hash value in case of an error - } -} +fun getGitHash(): String = + runGitCommand(listOf("git", "rev-parse", "--short", "HEAD")).ifEmpty { "dummy-hash" } + +fun getBranch(): String = + runGitCommand(listOf("git", "rev-parse", "--abbrev-ref", "HEAD")).ifEmpty { "dummy-hash" } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt new file mode 100644 index 000000000..f006f9e3d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder +import androidx.recyclerview.widget.RecyclerView + +/** + * Generic adapter that implements an [AsyncDifferConfig] and covers some of the basic boilerplate + * code used in every [RecyclerView]. + * Type assigned to [Model] must inherit from [Object] in order to be compared properly. + */ +abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>> : + ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>()).build()) { + override fun onBindViewHolder(holder: Holder, position: Int) = + holder.bind(currentList[position]) + + private class DiffCallback<Model> : DiffUtil.ItemCallback<Model>() { + override fun areItemsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean { + return oldItem === newItem + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean { + return oldItem == newItem + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractListAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractListAdapter.kt new file mode 100644 index 000000000..3dfee3d0c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractListAdapter.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder + +/** + * Generic list class meant to take care of basic lists + * @param currentList The list to show initially + */ +abstract class AbstractListAdapter<Model : Any, Holder : AbstractViewHolder<Model>>( + open var currentList: List<Model> +) : RecyclerView.Adapter<Holder>() { + override fun onBindViewHolder(holder: Holder, position: Int) = + holder.bind(currentList[position]) + + override fun getItemCount(): Int = currentList.size + + /** + * Adds an item to [currentList] and notifies the underlying adapter of the change. If no parameter + * is passed in for position, [item] is added to the end of the list. Invokes [callback] last. + * @param item The item to add to the list + * @param position Index where [item] will be added + * @param callback Lambda that's called at the end of the list changes and has the added list + * position passed in as a parameter + */ + open fun addItem(item: Model, position: Int = -1, callback: ((position: Int) -> Unit)? = null) { + val newList = currentList.toMutableList() + val positionToUpdate: Int + if (position == -1) { + newList.add(item) + currentList = newList + positionToUpdate = currentList.size - 1 + } else { + newList.add(position, item) + currentList = newList + positionToUpdate = position + } + onItemAdded(positionToUpdate, callback) + } + + protected fun onItemAdded(position: Int, callback: ((Int) -> Unit)? = null) { + notifyItemInserted(position) + callback?.invoke(position) + } + + /** + * Replaces the [item] at [position] in the [currentList] and notifies the underlying adapter + * of the change. Invokes [callback] last. + * @param item New list item + * @param position Index where [item] will replace the existing list item + * @param callback Lambda that's called at the end of the list changes and has the changed list + * position passed in as a parameter + */ + fun changeItem(item: Model, position: Int, callback: ((position: Int) -> Unit)? = null) { + val newList = currentList.toMutableList() + newList[position] = item + currentList = newList + onItemChanged(position, callback) + } + + protected fun onItemChanged(position: Int, callback: ((Int) -> Unit)? = null) { + notifyItemChanged(position) + callback?.invoke(position) + } + + /** + * Removes the list item at [position] in [currentList] and notifies the underlying adapter + * of the change. Invokes [callback] last. + * @param position Index where the list item will be removed + * @param callback Lambda that's called at the end of the list changes and has the removed list + * position passed in as a parameter + */ + fun removeItem(position: Int, callback: ((position: Int) -> Unit)? = null) { + val newList = currentList.toMutableList() + newList.removeAt(position) + currentList = newList + onItemRemoved(position, callback) + } + + protected fun onItemRemoved(position: Int, callback: ((Int) -> Unit)? = null) { + notifyItemRemoved(position) + callback?.invoke(position) + } + + /** + * Replaces [currentList] with [newList] and notifies the underlying adapter of the change. + * @param newList The new list to replace [currentList] + */ + @SuppressLint("NotifyDataSetChanged") + open fun replaceList(newList: List<Model>) { + currentList = newList + notifyDataSetChanged() + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractSingleSelectionList.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractSingleSelectionList.kt new file mode 100644 index 000000000..52163f9d7 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractSingleSelectionList.kt @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import org.yuzu.yuzu_emu.model.SelectableItem +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder + +/** + * Generic list class meant to take care of single selection UI updates + * @param currentList The list to show initially + * @param defaultSelection The default selection to use if no list items are selected by + * [SelectableItem.selected] or if the currently selected item is removed from the list + */ +abstract class AbstractSingleSelectionList< + Model : SelectableItem, + Holder : AbstractViewHolder<Model> + >( + final override var currentList: List<Model>, + private val defaultSelection: DefaultSelection = DefaultSelection.Start +) : AbstractListAdapter<Model, Holder>(currentList) { + var selectedItem = getDefaultSelection() + + init { + findSelectedItem() + } + + /** + * Changes the selection state of the [SelectableItem] that was selected and the previously selected + * item and notifies the underlying adapter of the change for those items. Invokes [callback] last. + * Does nothing if [position] is the same as the currently selected item. + * @param position Index of the item that was selected + * @param callback Lambda that's called at the end of the list changes and has the selected list + * position passed in as a parameter + */ + fun selectItem(position: Int, callback: ((position: Int) -> Unit)? = null) { + if (position == selectedItem) { + return + } + + val previouslySelectedItem = selectedItem + selectedItem = position + if (currentList.indices.contains(selectedItem)) { + currentList[selectedItem].onSelectionStateChanged(true) + } + if (currentList.indices.contains(previouslySelectedItem)) { + currentList[previouslySelectedItem].onSelectionStateChanged(false) + } + onItemChanged(previouslySelectedItem) + onItemChanged(selectedItem) + callback?.invoke(position) + } + + /** + * Removes a given item from the list and notifies the underlying adapter of the change. If the + * currently selected item was the item that was removed, the item at the position provided + * by [defaultSelection] will be made the new selection. Invokes [callback] last. + * @param position Index of the item that was removed + * @param callback Lambda that's called at the end of the list changes and has the removed and + * selected list positions passed in as parameters + */ + fun removeSelectableItem( + position: Int, + callback: ((removedPosition: Int, selectedPosition: Int) -> Unit)? + ) { + removeItem(position) + if (position == selectedItem) { + selectedItem = getDefaultSelection() + currentList[selectedItem].onSelectionStateChanged(true) + onItemChanged(selectedItem) + } else if (position < selectedItem) { + selectedItem-- + } + callback?.invoke(position, selectedItem) + } + + override fun addItem(item: Model, position: Int, callback: ((Int) -> Unit)?) { + super.addItem(item, position, callback) + if (position <= selectedItem && position != -1) { + selectedItem++ + } + } + + override fun replaceList(newList: List<Model>) { + super.replaceList(newList) + findSelectedItem() + } + + private fun findSelectedItem() { + for (i in currentList.indices) { + if (currentList[i].selected) { + selectedItem = i + break + } + } + } + + private fun getDefaultSelection(): Int = + when (defaultSelection) { + DefaultSelection.Start -> currentList.indices.first + DefaultSelection.End -> currentList.indices.last + } + + enum class DefaultSelection { Start, End } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt index 15c7ca3c9..94c151325 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt @@ -5,48 +5,28 @@ package org.yuzu.yuzu_emu.adapters import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding import org.yuzu.yuzu_emu.model.Addon +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>( - AsyncDifferConfig.Builder(DiffCallback()).build() -) { +class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) .also { return AddonViewHolder(it) } } - override fun getItemCount(): Int = currentList.size - - override fun onBindViewHolder(holder: AddonViewHolder, position: Int) = - holder.bind(currentList[position]) - inner class AddonViewHolder(val binding: ListItemAddonBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(addon: Addon) { + AbstractViewHolder<Addon>(binding) { + override fun bind(model: Addon) { binding.root.setOnClickListener { binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked } - binding.title.text = addon.title - binding.version.text = addon.version + binding.title.text = model.title + binding.version.text = model.version binding.addonSwitch.setOnCheckedChangeListener { _, checked -> - addon.enabled = checked + model.enabled = checked } - binding.addonSwitch.isChecked = addon.enabled - } - } - - private class DiffCallback : DiffUtil.ItemCallback<Addon>() { - override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean { - return oldItem == newItem + binding.addonSwitch.isChecked = model.enabled } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt index 4a05c5be9..41d7f72b8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt @@ -4,13 +4,11 @@ package org.yuzu.yuzu_emu.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.FragmentActivity import androidx.navigation.findNavController -import androidx.recyclerview.widget.RecyclerView import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -19,72 +17,58 @@ import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding import org.yuzu.yuzu_emu.model.Applet import org.yuzu.yuzu_emu.model.AppletInfo import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) : - RecyclerView.Adapter<AppletAdapter.AppletViewHolder>(), - View.OnClickListener { - +class AppletAdapter(val activity: FragmentActivity, applets: List<Applet>) : + AbstractListAdapter<Applet, AppletAdapter.AppletViewHolder>(applets) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): AppletAdapter.AppletViewHolder { CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false) - .apply { root.setOnClickListener(this@AppletAdapter) } .also { return AppletViewHolder(it) } } - override fun onBindViewHolder(holder: AppletViewHolder, position: Int) = - holder.bind(applets[position]) - - override fun getItemCount(): Int = applets.size - - override fun onClick(view: View) { - val applet = (view.tag as AppletViewHolder).applet - val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId) - if (appletPath.isEmpty()) { - Toast.makeText( - YuzuApplication.appContext, - R.string.applets_error_applet, - Toast.LENGTH_SHORT - ).show() - return - } - - if (applet.appletInfo == AppletInfo.Cabinet) { - view.findNavController() - .navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment) - return - } - - NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId) - val appletGame = Game( - title = YuzuApplication.appContext.getString(applet.titleId), - path = appletPath - ) - val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) - view.findNavController().navigate(action) - } - inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) : - RecyclerView.ViewHolder(binding.root) { - lateinit var applet: Applet - - init { - itemView.tag = this - } - - fun bind(applet: Applet) { - this.applet = applet - - binding.title.setText(applet.titleId) - binding.description.setText(applet.descriptionId) + AbstractViewHolder<Applet>(binding) { + override fun bind(model: Applet) { + binding.title.setText(model.titleId) + binding.description.setText(model.descriptionId) binding.icon.setImageDrawable( ResourcesCompat.getDrawable( binding.icon.context.resources, - applet.iconId, + model.iconId, binding.icon.context.theme ) ) + + binding.root.setOnClickListener { onClick(model) } + } + + fun onClick(applet: Applet) { + val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId) + if (appletPath.isEmpty()) { + Toast.makeText( + binding.root.context, + R.string.applets_error_applet, + Toast.LENGTH_SHORT + ).show() + return + } + + if (applet.appletInfo == AppletInfo.Cabinet) { + binding.root.findNavController() + .navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment) + return + } + + NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId) + val appletGame = Game( + title = YuzuApplication.appContext.getString(applet.titleId), + path = appletPath + ) + val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) + binding.root.findNavController().navigate(action) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt index e7b7c0f2f..a56137148 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/CabinetLauncherDialogAdapter.kt @@ -4,12 +4,10 @@ package org.yuzu.yuzu_emu.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.RecyclerView import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -19,54 +17,43 @@ import org.yuzu.yuzu_emu.model.CabinetMode import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder import org.yuzu.yuzu_emu.model.AppletInfo import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class CabinetLauncherDialogAdapter(val fragment: Fragment) : - RecyclerView.Adapter<CabinetModeViewHolder>(), - View.OnClickListener { - private val cabinetModes = CabinetMode.values().copyOfRange(1, CabinetMode.values().size) + AbstractListAdapter<CabinetMode, CabinetModeViewHolder>( + CabinetMode.values().copyOfRange(1, CabinetMode.entries.size).toList() + ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder { DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - .apply { root.setOnClickListener(this@CabinetLauncherDialogAdapter) } .also { return CabinetModeViewHolder(it) } } - override fun getItemCount(): Int = cabinetModes.size - - override fun onBindViewHolder(holder: CabinetModeViewHolder, position: Int) = - holder.bind(cabinetModes[position]) - - override fun onClick(view: View) { - val mode = (view.tag as CabinetModeViewHolder).cabinetMode - val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId) - NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId) - NativeLibrary.setCabinetMode(mode.id) - val appletGame = Game( - title = YuzuApplication.appContext.getString(R.string.cabinet_applet), - path = appletPath - ) - val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) - fragment.findNavController().navigate(action) - } - inner class CabinetModeViewHolder(val binding: DialogListItemBinding) : - RecyclerView.ViewHolder(binding.root) { - lateinit var cabinetMode: CabinetMode - - init { - itemView.tag = this - } - - fun bind(cabinetMode: CabinetMode) { - this.cabinetMode = cabinetMode + AbstractViewHolder<CabinetMode>(binding) { + override fun bind(model: CabinetMode) { binding.icon.setImageDrawable( ResourcesCompat.getDrawable( binding.icon.context.resources, - cabinetMode.iconId, + model.iconId, binding.icon.context.theme ) ) - binding.title.setText(cabinetMode.titleId) + binding.title.setText(model.titleId) + + binding.root.setOnClickListener { onClick(model) } + } + + private fun onClick(mode: CabinetMode) { + val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId) + NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId) + NativeLibrary.setCabinetMode(mode.id) + val appletGame = Game( + title = YuzuApplication.appContext.getString(R.string.cabinet_applet), + path = appletPath + ) + val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) + fragment.findNavController().navigate(action) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt index d290a656c..d6f17cf29 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt @@ -7,65 +7,39 @@ import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding +import org.yuzu.yuzu_emu.features.settings.model.StringSetting +import org.yuzu.yuzu_emu.model.Driver import org.yuzu.yuzu_emu.model.DriverViewModel -import org.yuzu.yuzu_emu.utils.GpuDriverHelper -import org.yuzu.yuzu_emu.utils.GpuDriverMetadata +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class DriverAdapter(private val driverViewModel: DriverViewModel) : - ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>( - AsyncDifferConfig.Builder(DiffCallback()).build() + AbstractSingleSelectionList<Driver, DriverAdapter.DriverViewHolder>( + driverViewModel.driverList.value ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder { - val binding = - CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return DriverViewHolder(binding) - } - - override fun getItemCount(): Int = currentList.size - - override fun onBindViewHolder(holder: DriverViewHolder, position: Int) = - holder.bind(currentList[position]) - - private fun onSelectDriver(position: Int) { - driverViewModel.setSelectedDriverIndex(position) - notifyItemChanged(driverViewModel.previouslySelectedDriver) - notifyItemChanged(driverViewModel.selectedDriver) - } - - private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) { - if (driverViewModel.selectedDriver > position) { - driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1) - } - if (GpuDriverHelper.customDriverSettingData == driverData.second) { - driverViewModel.setSelectedDriverIndex(0) - } - driverViewModel.driversToDelete.add(driverData.first) - driverViewModel.removeDriver(driverData) - notifyItemRemoved(position) - notifyItemChanged(driverViewModel.selectedDriver) + CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return DriverViewHolder(it) } } inner class DriverViewHolder(val binding: CardDriverOptionBinding) : - RecyclerView.ViewHolder(binding.root) { - private lateinit var driverData: Pair<String, GpuDriverMetadata> - - fun bind(driverData: Pair<String, GpuDriverMetadata>) { - this.driverData = driverData - val driver = driverData.second - + AbstractViewHolder<Driver>(binding) { + override fun bind(model: Driver) { binding.apply { - radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition + radioButton.isChecked = model.selected root.setOnClickListener { - onSelectDriver(bindingAdapterPosition) + selectItem(bindingAdapterPosition) { + driverViewModel.onDriverSelected(it) + driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global) + } } buttonDelete.setOnClickListener { - onDeleteDriver(driverData, bindingAdapterPosition) + removeSelectableItem( + bindingAdapterPosition + ) { removedPosition: Int, selectedPosition: Int -> + driverViewModel.onDriverRemoved(removedPosition, selectedPosition) + driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global) + } } // Delay marquee by 3s @@ -80,38 +54,19 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) : }, 3000 ) - if (driver.name == null) { - title.setText(R.string.system_gpu_driver) - description.text = "" - version.text = "" - version.visibility = View.GONE - description.visibility = View.GONE - buttonDelete.visibility = View.GONE - } else { - title.text = driver.name - version.text = driver.version - description.text = driver.description + title.text = model.title + version.text = model.version + description.text = model.description + if (model.description.isNotEmpty()) { version.visibility = View.VISIBLE description.visibility = View.VISIBLE buttonDelete.visibility = View.VISIBLE + } else { + version.visibility = View.GONE + description.visibility = View.GONE + buttonDelete.visibility = View.GONE } } } } - - private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() { - override fun areItemsTheSame( - oldItem: Pair<String, GpuDriverMetadata>, - newItem: Pair<String, GpuDriverMetadata> - ): Boolean { - return oldItem.first == newItem.first - } - - override fun areContentsTheSame( - oldItem: Pair<String, GpuDriverMetadata>, - newItem: Pair<String, GpuDriverMetadata> - ): Boolean { - return oldItem.second == newItem.second - } - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt index ab657a7b9..3d8f0bda8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt @@ -8,19 +8,14 @@ import android.text.TextUtils import android.view.LayoutInflater import android.view.ViewGroup import androidx.fragment.app.FragmentActivity -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView import org.yuzu.yuzu_emu.databinding.CardFolderBinding import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : - ListAdapter<GameDir, FolderAdapter.FolderViewHolder>( - AsyncDifferConfig.Builder(DiffCallback()).build() - ) { + AbstractDiffAdapter<GameDir, FolderAdapter.FolderViewHolder>() { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int @@ -29,18 +24,11 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie .also { return FolderViewHolder(it) } } - override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) = - holder.bind(currentList[position]) - inner class FolderViewHolder(val binding: CardFolderBinding) : - RecyclerView.ViewHolder(binding.root) { - private lateinit var gameDir: GameDir - - fun bind(gameDir: GameDir) { - this.gameDir = gameDir - + AbstractViewHolder<GameDir>(binding) { + override fun bind(model: GameDir) { binding.apply { - path.text = Uri.parse(gameDir.uriString).path + path.text = Uri.parse(model.uriString).path path.postDelayed( { path.isSelected = true @@ -50,7 +38,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie ) buttonEdit.setOnClickListener { - GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir) + GameFolderPropertiesDialogFragment.newInstance(model) .show( activity.supportFragmentManager, GameFolderPropertiesDialogFragment.TAG @@ -58,19 +46,9 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie } buttonDelete.setOnClickListener { - gamesViewModel.removeFolder(this@FolderViewHolder.gameDir) + gamesViewModel.removeFolder(model) } } } } - - private class DiffCallback : DiffUtil.ItemCallback<GameDir>() { - override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean { - return oldItem == newItem - } - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt index a578f0de8..e26c2e0ab 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt @@ -9,7 +9,6 @@ import android.graphics.drawable.LayerDrawable import android.net.Uri import android.text.TextUtils import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.Toast @@ -25,10 +24,6 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -36,122 +31,26 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.activities.EmulationActivity -import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder import org.yuzu.yuzu_emu.databinding.CardGameBinding import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.utils.GameIconUtils +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class GameAdapter(private val activity: AppCompatActivity) : - ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), - View.OnClickListener, - View.OnLongClickListener { + AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { - // Create a new view. - val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) - binding.cardGame.setOnClickListener(this) - binding.cardGame.setOnLongClickListener(this) - - // Use that view to create a ViewHolder. - return GameViewHolder(binding) - } - - override fun onBindViewHolder(holder: GameViewHolder, position: Int) = - holder.bind(currentList[position]) - - override fun getItemCount(): Int = currentList.size - - /** - * Launches the game that was clicked on. - * - * @param view The card representing the game the user wants to play. - */ - override fun onClick(view: View) { - val holder = view.tag as GameViewHolder - - val gameExists = DocumentFile.fromSingleUri( - YuzuApplication.appContext, - Uri.parse(holder.game.path) - )?.exists() == true - if (!gameExists) { - Toast.makeText( - YuzuApplication.appContext, - R.string.loader_error_file_not_found, - Toast.LENGTH_LONG - ).show() - - ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) - return - } - - val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - preferences.edit() - .putLong( - holder.game.keyLastPlayedTime, - System.currentTimeMillis() - ) - .apply() - - val openIntent = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply { - action = Intent.ACTION_VIEW - data = Uri.parse(holder.game.path) - } - - activity.lifecycleScope.launch { - withContext(Dispatchers.IO) { - val layerDrawable = ResourcesCompat.getDrawable( - YuzuApplication.appContext.resources, - R.drawable.shortcut, - null - ) as LayerDrawable - layerDrawable.setDrawableByLayerId( - R.id.shortcut_foreground, - GameIconUtils.getGameIcon(activity, holder.game) - .toDrawable(YuzuApplication.appContext.resources) - ) - val inset = YuzuApplication.appContext.resources - .getDimensionPixelSize(R.dimen.icon_inset) - layerDrawable.setLayerInset(1, inset, inset, inset, inset) - val shortcut = - ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path) - .setShortLabel(holder.game.title) - .setIcon( - IconCompat.createWithAdaptiveBitmap( - layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888) - ) - ) - .setIntent(openIntent) - .build() - ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut) - } - } - - val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true) - view.findNavController().navigate(action) - } - - override fun onLongClick(view: View): Boolean { - val holder = view.tag as GameViewHolder - val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game) - view.findNavController().navigate(action) - return true + CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return GameViewHolder(it) } } inner class GameViewHolder(val binding: CardGameBinding) : - RecyclerView.ViewHolder(binding.root) { - lateinit var game: Game - - init { - binding.cardGame.tag = this - } - - fun bind(game: Game) { - this.game = game - + AbstractViewHolder<Game>(binding) { + override fun bind(model: Game) { binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP - GameIconUtils.loadGameIcon(game, binding.imageGameScreen) + GameIconUtils.loadGameIcon(model, binding.imageGameScreen) - binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") + binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") binding.textGameTitle.postDelayed( { @@ -160,16 +59,79 @@ class GameAdapter(private val activity: AppCompatActivity) : }, 3000 ) + + binding.cardGame.setOnClickListener { onClick(model) } + binding.cardGame.setOnLongClickListener { onLongClick(model) } } - } - private class DiffCallback : DiffUtil.ItemCallback<Game>() { - override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { - return oldItem == newItem + fun onClick(game: Game) { + val gameExists = DocumentFile.fromSingleUri( + YuzuApplication.appContext, + Uri.parse(game.path) + )?.exists() == true + if (!gameExists) { + Toast.makeText( + YuzuApplication.appContext, + R.string.loader_error_file_not_found, + Toast.LENGTH_LONG + ).show() + + ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) + return + } + + val preferences = + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + preferences.edit() + .putLong( + game.keyLastPlayedTime, + System.currentTimeMillis() + ) + .apply() + + val openIntent = + Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply { + action = Intent.ACTION_VIEW + data = Uri.parse(game.path) + } + + activity.lifecycleScope.launch { + withContext(Dispatchers.IO) { + val layerDrawable = ResourcesCompat.getDrawable( + YuzuApplication.appContext.resources, + R.drawable.shortcut, + null + ) as LayerDrawable + layerDrawable.setDrawableByLayerId( + R.id.shortcut_foreground, + GameIconUtils.getGameIcon(activity, game) + .toDrawable(YuzuApplication.appContext.resources) + ) + val inset = YuzuApplication.appContext.resources + .getDimensionPixelSize(R.dimen.icon_inset) + layerDrawable.setLayerInset(1, inset, inset, inset, inset) + val shortcut = + ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path) + .setShortLabel(game.title) + .setIcon( + IconCompat.createWithAdaptiveBitmap( + layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888) + ) + ) + .setIntent(openIntent) + .build() + ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut) + } + } + + val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) + binding.root.findNavController().navigate(action) } - override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { - return oldItem == newItem + fun onLongClick(game: Game): Boolean { + val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game) + binding.root.findNavController().navigate(action) + return true } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt index 95841d786..0046d5314 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt @@ -12,23 +12,22 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding import org.yuzu.yuzu_emu.model.GameProperty import org.yuzu.yuzu_emu.model.InstallableProperty import org.yuzu.yuzu_emu.model.SubmenuProperty +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class GamePropertiesAdapter( private val viewLifecycle: LifecycleOwner, private var properties: List<GameProperty> -) : - RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() { +) : AbstractListAdapter<GameProperty, AbstractViewHolder<GameProperty>>(properties) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int - ): GamePropertyViewHolder { + ): AbstractViewHolder<GameProperty> { val inflater = LayoutInflater.from(parent.context) return when (viewType) { PropertyType.Submenu.ordinal -> { @@ -51,11 +50,6 @@ class GamePropertiesAdapter( } } - override fun getItemCount(): Int = properties.size - - override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) = - holder.bind(properties[position]) - override fun getItemViewType(position: Int): Int { return when (properties[position]) { is SubmenuProperty -> PropertyType.Submenu.ordinal @@ -63,14 +57,10 @@ class GamePropertiesAdapter( } } - sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - abstract fun bind(property: GameProperty) - } - inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) : - GamePropertyViewHolder(binding.root) { - override fun bind(property: GameProperty) { - val submenuProperty = property as SubmenuProperty + AbstractViewHolder<GameProperty>(binding) { + override fun bind(model: GameProperty) { + val submenuProperty = model as SubmenuProperty binding.root.setOnClickListener { submenuProperty.action.invoke() @@ -108,9 +98,9 @@ class GamePropertiesAdapter( } inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) : - GamePropertyViewHolder(binding.root) { - override fun bind(property: GameProperty) { - val installableProperty = property as InstallableProperty + AbstractViewHolder<GameProperty>(binding) { + override fun bind(model: GameProperty) { + val installableProperty = model as InstallableProperty binding.title.setText(installableProperty.titleId) binding.description.setText(installableProperty.descriptionId) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index 58ce343f4..b512845d5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt @@ -14,69 +14,37 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.HomeSetting +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class HomeSettingAdapter( private val activity: AppCompatActivity, private val viewLifecycle: LifecycleOwner, - var options: List<HomeSetting> -) : - RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(), - View.OnClickListener { + options: List<HomeSetting> +) : AbstractListAdapter<HomeSetting, HomeSettingAdapter.HomeOptionViewHolder>(options) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { - val binding = - CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) - binding.root.setOnClickListener(this) - return HomeOptionViewHolder(binding) - } - - override fun getItemCount(): Int { - return options.size - } - - override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) { - holder.bind(options[position]) - } - - override fun onClick(view: View) { - val holder = view.tag as HomeOptionViewHolder - if (holder.option.isEnabled.invoke()) { - holder.option.onClick.invoke() - } else { - MessageDialogFragment.newInstance( - activity, - titleId = holder.option.disabledTitleId, - descriptionId = holder.option.disabledMessageId - ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) - } + CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return HomeOptionViewHolder(it) } } inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) : - RecyclerView.ViewHolder(binding.root) { - lateinit var option: HomeSetting - - init { - itemView.tag = this - } - - fun bind(option: HomeSetting) { - this.option = option - binding.optionTitle.text = activity.resources.getString(option.titleId) - binding.optionDescription.text = activity.resources.getString(option.descriptionId) + AbstractViewHolder<HomeSetting>(binding) { + override fun bind(model: HomeSetting) { + binding.optionTitle.text = activity.resources.getString(model.titleId) + binding.optionDescription.text = activity.resources.getString(model.descriptionId) binding.optionIcon.setImageDrawable( ResourcesCompat.getDrawable( activity.resources, - option.iconId, + model.iconId, activity.theme ) ) - when (option.titleId) { + when (model.titleId) { R.string.get_early_access -> binding.optionLayout.background = ContextCompat.getDrawable( @@ -85,7 +53,7 @@ class HomeSettingAdapter( ) } - if (!option.isEnabled.invoke()) { + if (!model.isEnabled.invoke()) { binding.optionTitle.alpha = 0.5f binding.optionDescription.alpha = 0.5f binding.optionIcon.alpha = 0.5f @@ -93,7 +61,7 @@ class HomeSettingAdapter( viewLifecycle.lifecycleScope.launch { viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { - option.details.collect { updateOptionDetails(it) } + model.details.collect { updateOptionDetails(it) } } } binding.optionDetail.postDelayed( @@ -103,6 +71,20 @@ class HomeSettingAdapter( }, 3000 ) + + binding.root.setOnClickListener { onClick(model) } + } + + private fun onClick(model: HomeSetting) { + if (model.isEnabled.invoke()) { + model.onClick.invoke() + } else { + MessageDialogFragment.newInstance( + activity, + titleId = model.disabledTitleId, + descriptionId = model.disabledMessageId + ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) + } } private fun updateOptionDetails(detailString: String) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt index e960fbaab..4218c4e52 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt @@ -6,43 +6,33 @@ package org.yuzu.yuzu_emu.adapters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView import org.yuzu.yuzu_emu.databinding.CardInstallableBinding import org.yuzu.yuzu_emu.model.Installable +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class InstallableAdapter(private val installables: List<Installable>) : - RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { +class InstallableAdapter(installables: List<Installable>) : + AbstractListAdapter<Installable, InstallableAdapter.InstallableViewHolder>(installables) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): InstallableAdapter.InstallableViewHolder { - val binding = - CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return InstallableViewHolder(binding) + CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return InstallableViewHolder(it) } } - override fun getItemCount(): Int = installables.size - - override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) = - holder.bind(installables[position]) - inner class InstallableViewHolder(val binding: CardInstallableBinding) : - RecyclerView.ViewHolder(binding.root) { - lateinit var installable: Installable - - fun bind(installable: Installable) { - this.installable = installable - - binding.title.setText(installable.titleId) - binding.description.setText(installable.descriptionId) + AbstractViewHolder<Installable>(binding) { + override fun bind(model: Installable) { + binding.title.setText(model.titleId) + binding.description.setText(model.descriptionId) - if (installable.install != null) { + if (model.install != null) { binding.buttonInstall.visibility = View.VISIBLE - binding.buttonInstall.setOnClickListener { installable.install.invoke() } + binding.buttonInstall.setOnClickListener { model.install.invoke() } } - if (installable.export != null) { + if (model.export != null) { binding.buttonExport.visibility = View.VISIBLE - binding.buttonExport.setOnClickListener { installable.export.invoke() } + binding.buttonExport.setOnClickListener { model.export.invoke() } } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt index bc6ff1364..38bb1f96f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt @@ -7,49 +7,33 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment import org.yuzu.yuzu_emu.model.License +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) : - RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(), - View.OnClickListener { +class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) : + AbstractListAdapter<License, LicenseAdapter.LicenseViewHolder>(licenses) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder { - val binding = - ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false) - binding.root.setOnClickListener(this) - return LicenseViewHolder(binding) + ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return LicenseViewHolder(it) } } - override fun getItemCount(): Int = licenses.size + inner class LicenseViewHolder(val binding: ListItemSettingBinding) : + AbstractViewHolder<License>(binding) { + override fun bind(model: License) { + binding.apply { + textSettingName.text = root.context.getString(model.titleId) + textSettingDescription.text = root.context.getString(model.descriptionId) + textSettingValue.visibility = View.GONE - override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) { - holder.bind(licenses[position]) - } - - override fun onClick(view: View) { - val license = (view.tag as LicenseViewHolder).license - LicenseBottomSheetDialogFragment.newInstance(license) - .show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG) - } - - inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) { - lateinit var license: License - - init { - itemView.tag = this + root.setOnClickListener { onClick(model) } + } } - fun bind(license: License) { - this.license = license - - val context = YuzuApplication.appContext - binding.textSettingName.text = context.getString(license.titleId) - binding.textSettingDescription.text = context.getString(license.descriptionId) - binding.textSettingValue.visibility = View.GONE + private fun onClick(license: License) { + LicenseBottomSheetDialogFragment.newInstance(license) + .show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt index 6b46d359e..02118e1a8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt @@ -10,7 +10,6 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import org.yuzu.yuzu_emu.databinding.PageSetupBinding import org.yuzu.yuzu_emu.model.HomeViewModel @@ -18,31 +17,19 @@ import org.yuzu.yuzu_emu.model.SetupCallback import org.yuzu.yuzu_emu.model.SetupPage import org.yuzu.yuzu_emu.model.StepState import org.yuzu.yuzu_emu.utils.ViewUtils +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) : - RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() { +class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : + AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder { - val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return SetupPageViewHolder(binding) + PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return SetupPageViewHolder(it) } } - override fun getItemCount(): Int = pages.size - - override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) = - holder.bind(pages[position]) - inner class SetupPageViewHolder(val binding: PageSetupBinding) : - RecyclerView.ViewHolder(binding.root), SetupCallback { - lateinit var page: SetupPage - - init { - itemView.tag = this - } - - fun bind(page: SetupPage) { - this.page = page - - if (page.stepCompleted.invoke() == StepState.COMPLETE) { + AbstractViewHolder<SetupPage>(binding), SetupCallback { + override fun bind(model: SetupPage) { + if (model.stepCompleted.invoke() == StepState.COMPLETE) { binding.buttonAction.visibility = View.INVISIBLE binding.textConfirmation.visibility = View.VISIBLE } @@ -50,31 +37,31 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) binding.icon.setImageDrawable( ResourcesCompat.getDrawable( activity.resources, - page.iconId, + model.iconId, activity.theme ) ) - binding.textTitle.text = activity.resources.getString(page.titleId) + binding.textTitle.text = activity.resources.getString(model.titleId) binding.textDescription.text = - Html.fromHtml(activity.resources.getString(page.descriptionId), 0) + Html.fromHtml(activity.resources.getString(model.descriptionId), 0) binding.buttonAction.apply { - text = activity.resources.getString(page.buttonTextId) - if (page.buttonIconId != 0) { + text = activity.resources.getString(model.buttonTextId) + if (model.buttonIconId != 0) { icon = ResourcesCompat.getDrawable( activity.resources, - page.buttonIconId, + model.buttonIconId, activity.theme ) } iconGravity = - if (page.leftAlignedIcon) { + if (model.leftAlignedIcon) { MaterialButton.ICON_GRAVITY_START } else { MaterialButton.ICON_GRAVITY_END } setOnClickListener { - page.buttonAction.invoke(this@SetupPageViewHolder) + model.buttonAction.invoke(this@SetupPageViewHolder) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt index a1620fbb7..5b5f800c1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt @@ -76,8 +76,8 @@ class AboutFragment : Fragment() { binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment) } - binding.textBuildHash.text = BuildConfig.GIT_HASH - binding.buttonBuildHash.setOnClickListener { + binding.textVersionName.text = BuildConfig.VERSION_NAME + binding.textVersionName.setOnClickListener { val clipBoard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText(getString(R.string.build), BuildConfig.GIT_HASH) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index cc71254dc..9dabb9c41 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.fragments +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -13,20 +14,26 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.DriverAdapter import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding +import org.yuzu.yuzu_emu.features.settings.model.StringSetting +import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.utils.NativeConfig import java.io.File import java.io.IOException @@ -55,12 +62,43 @@ class DriverManagerFragment : Fragment() { return binding.root } + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) homeViewModel.setNavigationVisibility(visible = false, animated = true) homeViewModel.setStatusBarShadeVisibility(visible = false) driverViewModel.onOpenDriverManager(args.game) + if (NativeConfig.isPerGameConfigLoaded()) { + binding.toolbarDrivers.inflateMenu(R.menu.menu_driver_manager) + driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global) + binding.toolbarDrivers.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_driver_clear -> { + StringSetting.DRIVER_PATH.global = true + driverViewModel.updateDriverList() + (binding.listDrivers.adapter as DriverAdapter) + .replaceList(driverViewModel.driverList.value) + driverViewModel.showClearButton(false) + true + } + + else -> false + } + } + + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + driverViewModel.showClearButton.collect { + binding.toolbarDrivers.menu + .findItem(R.id.menu_driver_clear).isVisible = it + } + } + } + } + } if (!driverViewModel.isInteractionAllowed.value) { DriversLoadingDialogFragment().show( @@ -85,25 +123,6 @@ class DriverManagerFragment : Fragment() { adapter = DriverAdapter(driverViewModel) } - viewLifecycleOwner.lifecycleScope.apply { - launch { - driverViewModel.driverList.collectLatest { - (binding.listDrivers.adapter as DriverAdapter).submitList(it) - } - } - launch { - driverViewModel.newDriverInstalled.collect { - if (_binding != null && it) { - (binding.listDrivers.adapter as DriverAdapter).apply { - notifyItemChanged(driverViewModel.previouslySelectedDriver) - notifyItemChanged(driverViewModel.selectedDriver) - driverViewModel.setNewDriverInstalled(false) - } - } - } - } - } - setInsets() } @@ -160,7 +179,7 @@ class DriverManagerFragment : Fragment() { false ) { val driverPath = - "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}" + "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}" val driverFile = File(driverPath) // Ignore file exceptions when a user selects an invalid zip @@ -177,12 +196,21 @@ class DriverManagerFragment : Fragment() { val driverData = GpuDriverHelper.getMetadataFromZip(driverFile) val driverInList = - driverViewModel.driverList.value.firstOrNull { it.second == driverData } + driverViewModel.driverData.firstOrNull { it.second == driverData } if (driverInList != null) { return@newInstance getString(R.string.driver_already_installed) } else { - driverViewModel.addDriver(Pair(driverPath, driverData)) - driverViewModel.setNewDriverInstalled(true) + driverViewModel.onDriverAdded(Pair(driverPath, driverData)) + withContext(Dispatchers.Main) { + if (_binding != null) { + val adapter = binding.listDrivers.adapter as DriverAdapter + adapter.addItem(driverData.toDriver()) + adapter.selectItem(adapter.currentList.indices.last) + driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global) + binding.listDrivers + .smoothScrollToPosition(adapter.currentList.indices.last) + } + } } return@newInstance Any() }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Driver.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Driver.kt new file mode 100644 index 000000000..de342212a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Driver.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import org.yuzu.yuzu_emu.utils.GpuDriverMetadata + +data class Driver( + override var selected: Boolean, + val title: String, + val version: String = "", + val description: String = "" +) : SelectableItem { + override fun onSelectionStateChanged(selected: Boolean) { + this.selected = selected + } + + companion object { + fun GpuDriverMetadata.toDriver(selected: Boolean = false): Driver = + Driver( + selected, + this.name ?: "", + this.version ?: "", + this.description ?: "" + ) + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt index 76accf8f3..15ae3a42b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -17,11 +18,10 @@ import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile -import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.GpuDriverMetadata import org.yuzu.yuzu_emu.utils.NativeConfig -import java.io.BufferedOutputStream import java.io.File class DriverViewModel : ViewModel() { @@ -38,97 +38,81 @@ class DriverViewModel : ViewModel() { !loading && ready && !deleting }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false) - private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers()) - val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList + var driverData = GpuDriverHelper.getDrivers() - var previouslySelectedDriver = 0 - var selectedDriver = -1 + private val _driverList = MutableStateFlow(emptyList<Driver>()) + val driverList: StateFlow<List<Driver>> get() = _driverList // Used for showing which driver is currently installed within the driver manager card private val _selectedDriverTitle = MutableStateFlow("") val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle - private val _newDriverInstalled = MutableStateFlow(false) - val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled + private val _showClearButton = MutableStateFlow(false) + val showClearButton = _showClearButton.asStateFlow() - val driversToDelete = mutableListOf<String>() + private val driversToDelete = mutableListOf<String>() init { - val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData - findSelectedDriver(currentDriverMetadata) - - // If a user had installed a driver before the manager was implemented, this zips - // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can - // be indexed and exported as expected. - if (selectedDriver == -1) { - val driverToSave = - File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip") - driverToSave.createNewFile() - FileUtil.zipFromInternalStorage( - File(GpuDriverHelper.driverInstallationPath!!), - GpuDriverHelper.driverInstallationPath!!, - BufferedOutputStream(driverToSave.outputStream()) - ) - _driverList.value.add(Pair(driverToSave.path, currentDriverMetadata)) - setSelectedDriverIndex(_driverList.value.size - 1) - } + updateDriverList() + updateDriverNameForGame(null) + } - // If a user had installed a driver before the config was reworked to be multiplatform, - // we have save the path of the previously selected driver to the new setting. - if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 && - StringSetting.DRIVER_PATH.global - ) { - StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first) - NativeConfig.saveGlobalConfig() - } else { - findSelectedDriver(GpuDriverHelper.customDriverSettingData) + fun reloadDriverData() { + _areDriversLoading.value = true + driverData = GpuDriverHelper.getDrivers() + updateDriverList() + _areDriversLoading.value = false + } + + fun updateDriverList() { + val selectedDriver = GpuDriverHelper.customDriverSettingData + val newDriverList = mutableListOf( + Driver( + selectedDriver == GpuDriverMetadata(), + YuzuApplication.appContext.getString(R.string.system_gpu_driver) + ) + ) + driverData.forEach { + newDriverList.add(it.second.toDriver(it.second == selectedDriver)) } - updateDriverNameForGame(null) + _driverList.value = newDriverList } - fun setSelectedDriverIndex(value: Int) { - if (selectedDriver != -1) { - previouslySelectedDriver = selectedDriver + fun onOpenDriverManager(game: Game?) { + if (game != null) { + SettingsFile.loadCustomConfig(game) } - selectedDriver = value + updateDriverList() } - fun setNewDriverInstalled(value: Boolean) { - _newDriverInstalled.value = value + fun showClearButton(value: Boolean) { + _showClearButton.value = value } - fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { - val driverIndex = _driverList.value.indexOfFirst { it == driverData } - if (driverIndex == -1) { - _driverList.value.add(driverData) - setSelectedDriverIndex(_driverList.value.size - 1) - _selectedDriverTitle.value = driverData.second.name - ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) + fun onDriverSelected(position: Int) { + if (position == 0) { + StringSetting.DRIVER_PATH.setString("") } else { - setSelectedDriverIndex(driverIndex) + StringSetting.DRIVER_PATH.setString(driverData[position - 1].first) } } - fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) { - _driverList.value.remove(driverData) + fun onDriverRemoved(removedPosition: Int, selectedPosition: Int) { + driversToDelete.add(driverData[removedPosition - 1].first) + driverData.removeAt(removedPosition - 1) + onDriverSelected(selectedPosition) } - fun onOpenDriverManager(game: Game?) { - if (game != null) { - SettingsFile.loadCustomConfig(game) - } - - val driverPath = StringSetting.DRIVER_PATH.getString() - if (driverPath.isEmpty()) { - setSelectedDriverIndex(0) - } else { - findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath))) + fun onDriverAdded(driver: Pair<String, GpuDriverMetadata>) { + if (driversToDelete.contains(driver.first)) { + driversToDelete.remove(driver.first) } + driverData.add(driver) + onDriverSelected(driverData.size) } fun onCloseDriverManager(game: Game?) { _isDeletingDrivers.value = true - StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first) updateDriverNameForGame(game) if (game == null) { NativeConfig.saveGlobalConfig() @@ -181,20 +165,6 @@ class DriverViewModel : ViewModel() { } } - private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) { - if (driverList.value.size == 1) { - setSelectedDriverIndex(0) - return - } - - driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> -> - if (driver.second == currentDriverMetadata) { - setSelectedDriverIndex(i) - return - } - } - } - fun updateDriverNameForGame(game: Game?) { if (!GpuDriverHelper.supportsCustomDriverLoading()) { return @@ -217,7 +187,6 @@ class DriverViewModel : ViewModel() { private fun setDriverReady() { _isDriverReady.value = true - _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name - ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) + updateName() } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SelectableItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SelectableItem.kt new file mode 100644 index 000000000..11c22d323 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SelectableItem.kt @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +interface SelectableItem { + var selected: Boolean + fun onSelectionStateChanged(selected: Boolean) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 622ae996e..644289e25 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -41,6 +41,7 @@ import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.AddonViewModel +import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.TaskState @@ -58,6 +59,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private val gamesViewModel: GamesViewModel by viewModels() private val taskViewModel: TaskViewModel by viewModels() private val addonViewModel: AddonViewModel by viewModels() + private val driverViewModel: DriverViewModel by viewModels() override var themeId: Int = 0 @@ -689,6 +691,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { NativeLibrary.initializeSystem(true) NativeConfig.initializeGlobalConfig() gamesViewModel.reloadGames(false) + driverViewModel.reloadDriverData() return@newInstance getString(R.string.user_data_import_success) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index 132f002fb..b54a19c65 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -104,7 +104,7 @@ object FileUtil { /** * Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow - * This function will be faster than DoucmentFile.listFiles + * This function will be faster than DocumentFile.listFiles * @param uri Directory uri. * @return CheapDocument lists. */ diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index 685272288..a8f9dcc34 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt @@ -62,9 +62,6 @@ object GpuDriverHelper { ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name } ?.distinct() ?.toMutableList() ?: mutableListOf() - - // TODO: Get system driver information - drivers.add(0, Pair("", GpuDriverMetadata())) return drivers } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholder/AbstractViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholder/AbstractViewHolder.kt new file mode 100644 index 000000000..7101ad434 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholder/AbstractViewHolder.kt @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.viewholder + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import org.yuzu.yuzu_emu.adapters.AbstractDiffAdapter +import org.yuzu.yuzu_emu.adapters.AbstractListAdapter + +/** + * [RecyclerView.ViewHolder] meant to work together with a [AbstractDiffAdapter] or a + * [AbstractListAdapter] so we can run [bind] on each list item without needing a manual hookup. + */ +abstract class AbstractViewHolder<Model>(binding: ViewBinding) : + RecyclerView.ViewHolder(binding.root) { + abstract fun bind(model: Model) +} diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 136c8dee6..ed3b1353a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -410,8 +410,8 @@ void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) { jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld); if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) { - handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController); - controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController); + handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); + controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); handheld->Disconnect(); } } @@ -770,8 +770,8 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* ASSERT(user_id); const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( - EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, - FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0); + {}, vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, 1, + user_id->AsU128(), 0); const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path); if (!Common::FS::CreateParentDirs(full_path)) { @@ -878,7 +878,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject j FileSys::Mode::Read); const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( - system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, + {}, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, program_id, user_id->AsU128(), 0); return ToJString(env, user_save_data_path); } diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 535902483..c6c3343dc 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -205,7 +205,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEn jstring jkey) { auto setting = getSetting<std::string>(env, jkey); if (setting != nullptr) { - return setting->RuntimeModfiable(); + return setting->RuntimeModifiable(); } return true; } diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_about.xml b/src/android/app/src/main/res/layout-w600dp/fragment_about.xml index a26ffbc73..655e49219 100644 --- a/src/android/app/src/main/res/layout-w600dp/fragment_about.xml +++ b/src/android/app/src/main/res/layout-w600dp/fragment_about.xml @@ -147,7 +147,7 @@ android:layout_marginHorizontal="20dp" /> <LinearLayout - android:id="@+id/button_build_hash" + android:id="@+id/button_version_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/selectableItemBackground" @@ -164,7 +164,7 @@ android:textAlignment="viewStart" /> <com.google.android.material.textview.MaterialTextView - android:id="@+id/text_build_hash" + android:id="@+id/text_version_name" style="@style/TextAppearance.Material3.BodyMedium" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml index a24f5230e..38090fa50 100644 --- a/src/android/app/src/main/res/layout/fragment_about.xml +++ b/src/android/app/src/main/res/layout/fragment_about.xml @@ -148,7 +148,7 @@ android:layout_marginHorizontal="20dp" /> <LinearLayout - android:id="@+id/button_build_hash" + android:id="@+id/button_version_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingVertical="16dp" @@ -165,7 +165,7 @@ android:text="@string/build" /> <com.google.android.material.textview.MaterialTextView - android:id="@+id/text_build_hash" + android:id="@+id/text_version_name" style="@style/TextAppearance.Material3.BodyMedium" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/src/android/app/src/main/res/menu/menu_driver_manager.xml b/src/android/app/src/main/res/menu/menu_driver_manager.xml new file mode 100644 index 000000000..dee5d57b6 --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_driver_manager.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/menu_driver_clear" + android:icon="@drawable/ic_clear" + android:title="@string/clear" + app:showAsAction="always" /> + +</menu> diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 0363ff3b6..4701913eb 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -29,7 +29,7 @@ <item>@string/language_dutch</item> <item>@string/language_english</item> <item>@string/language_french</item> - <item>@string/langauge_german</item> + <item>@string/language_german</item> <item>@string/language_italian</item> <item>@string/language_japanese</item> <item>@string/language_korean</item> @@ -228,10 +228,10 @@ <item>R</item> <item>ZL</item> <item>ZR</item> - <item>@string/gamepad_left_stick</item> - <item>@string/gamepad_right_stick</item> <item>L3</item> <item>R3</item> + <item>@string/gamepad_left_stick</item> + <item>@string/gamepad_right_stick</item> <item>@string/gamepad_d_pad</item> </string-array> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 3bb92ad67..547752bda 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -410,7 +410,7 @@ <string name="language_japanese" translatable="false">日本語</string> <string name="language_english" translatable="false">English</string> <string name="language_french" translatable="false">Français</string> - <string name="langauge_german" translatable="false">Deutsch</string> + <string name="language_german" translatable="false">Deutsch</string> <string name="language_italian" translatable="false">Italiano</string> <string name="language_spanish" translatable="false">Español</string> <string name="language_chinese" translatable="false">简体中文</string> |