diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt index 06105d2702..14d69cb384 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + package org.yuzu.yuzu_emu.features.fetcher import android.annotation.SuppressLint @@ -17,7 +20,9 @@ import androidx.transition.TransitionManager import androidx.transition.TransitionSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.model.DriverViewModel class DriverGroupAdapter( @@ -25,43 +30,61 @@ class DriverGroupAdapter( private val driverViewModel: DriverViewModel ) : RecyclerView.Adapter() { private var driverGroups: List = emptyList() + private val adapterJobs = mutableMapOf() inner class DriverGroupViewHolder( private val binding: ItemDriverGroupBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(group: DriverGroup) { + binding.textGroupName.text = group.name + + if (binding.recyclerReleases.layoutManager == null) { + binding.recyclerReleases.layoutManager = LinearLayoutManager(activity) + binding.recyclerReleases.addItemDecoration( + SpacingItemDecoration( + (activity.resources.displayMetrics.density * 8).toInt() + ) + ) + } + val onClick = { + adapterJobs[bindingAdapterPosition]?.cancel() + TransitionManager.beginDelayedTransition( binding.root, TransitionSet().addTransition(Fade()).addTransition(ChangeBounds()) .setDuration(200) ) + val isVisible = binding.recyclerReleases.isVisible + if (!isVisible && binding.recyclerReleases.adapter == null) { + val job = CoroutineScope(Dispatchers.Main).launch { + // It prevents blocking the ui thread. + var adapter: ReleaseAdapter? + + withContext(Dispatchers.IO) { + adapter = ReleaseAdapter(group.releases, activity, driverViewModel) + } + + binding.recyclerReleases.adapter = adapter + } + + adapterJobs[bindingAdapterPosition] = job + } + binding.recyclerReleases.visibility = if (isVisible) View.GONE else View.VISIBLE binding.imageDropdownArrow.rotation = if (isVisible) 0f else 180f - - if (!isVisible && binding.recyclerReleases.adapter == null) { - CoroutineScope(Dispatchers.Main).launch { - binding.recyclerReleases.layoutManager = - LinearLayoutManager(binding.root.context) - binding.recyclerReleases.adapter = - ReleaseAdapter(group.releases, activity, driverViewModel) - - binding.recyclerReleases.addItemDecoration( - SpacingItemDecoration( - (activity.resources.displayMetrics.density * 8).toInt() - ) - ) - } - } } - binding.textGroupName.text = group.name binding.textGroupName.setOnClickListener { onClick() } - binding.imageDropdownArrow.setOnClickListener { onClick() } } + + fun clear() { + adapterJobs[bindingAdapterPosition]?.cancel() + adapterJobs.remove(bindingAdapterPosition) + } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverGroupViewHolder { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt index 7c6dc238e3..0c1e39d095 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt @@ -1,6 +1,8 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + package org.yuzu.yuzu_emu.fragments -import android.app.AlertDialog import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater @@ -28,10 +30,12 @@ import org.yuzu.yuzu_emu.databinding.FragmentDriverFetcherBinding import org.yuzu.yuzu_emu.features.fetcher.DriverGroupAdapter import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.utils.GpuDriverHelper -import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import java.io.IOException import java.net.URL +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId import kotlin.getValue class DriverFetcherFragment : Fragment() { @@ -49,17 +53,22 @@ class DriverFetcherFragment : Fragment() { private val recommendedDriver: String get() = driverMap.firstOrNull { adrenoModel in it.first }?.second ?: "Unsupported" + enum class SortMode { + Default, PublishTime, + } + private data class DriverRepo( val name: String = "", val path: String = "", val sort: Int = 0, - val useTagName: Boolean = false + val useTagName: Boolean = false, + val sortMode: SortMode = SortMode.Default, ) private val repoList: List = listOf( DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0), DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1), - DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true), + DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true, SortMode.PublishTime), DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3), ) @@ -78,7 +87,7 @@ class DriverFetcherFragment : Fragment() { private lateinit var driverGroupAdapter: DriverGroupAdapter private val driverViewModel: DriverViewModel by activityViewModels() - fun parseAdrenoModel(): Int { + private fun parseAdrenoModel(): Int { if (gpuModel == null) { return 0 } @@ -115,9 +124,8 @@ class DriverFetcherFragment : Fragment() { } override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { _binding = FragmentDriverFetcherBinding.inflate(inflater) binding.badgeRecommendedDriver.text = recommendedDriver binding.badgeGpuModel.text = gpuModel @@ -150,11 +158,12 @@ class DriverFetcherFragment : Fragment() { val name = driver.name val path = driver.path val useTagName = driver.useTagName + val sortMode = driver.sortMode val sort = driver.sort + CoroutineScope(Dispatchers.Main).launch { - val request = Request.Builder() - .url("https://api.github.com/repos/$path/releases") - .build() + val request = + Request.Builder().url("https://api.github.com/repos/$path/releases").build() withContext(Dispatchers.IO) { var releases: ArrayList @@ -165,28 +174,25 @@ class DriverFetcherFragment : Fragment() { } val body = response.body?.string() ?: return@withContext - releases = Release.fromJsonArray(body, useTagName) + releases = Release.fromJsonArray(body, useTagName, sortMode) } } catch (e: Exception) { withContext(Dispatchers.Main) { - MaterialAlertDialogBuilder(requireActivity().applicationContext) - .setTitle(getString(R.string.error_during_fetch)) + MaterialAlertDialogBuilder(requireActivity()).setTitle(getString(R.string.error_during_fetch)) .setMessage("${getString(R.string.failed_to_fetch)} ${name}:\n${e.message}") .setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.cancel() } .show() - releases = ArrayList() + releases = ArrayList() } } - val driver = DriverGroup( - name, - releases, - sort + val group = DriverGroup( + name, releases, sort ) synchronized(driverGroups) { - driverGroups.add(driver) + driverGroups.add(group) driverGroups.sortBy { it.sort } @@ -204,39 +210,41 @@ class DriverFetcherFragment : Fragment() { } } - private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener( - binding.root - ) { _: View, windowInsets: WindowInsetsCompat -> - val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) - val leftInsets = barInsets.left + cutoutInsets.left - val rightInsets = barInsets.right + cutoutInsets.right + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right - binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets) - binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets) + binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets) + binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets) - binding.listDrivers.updatePadding( - bottom = barInsets.bottom + - resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) - ) + binding.listDrivers.updatePadding( + bottom = barInsets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) + ) - windowInsets - } + windowInsets + } data class Artifact(val url: URL, val name: String) data class Release( var tagName: String = "", + var titleName: String = "", var title: String = "", var body: String = "", - var artifacts: List = ArrayList(), + var artifacts: List = ArrayList(), var prerelease: Boolean = false, - var latest: Boolean = false + var latest: Boolean = false, + var publishTime: LocalDateTime = LocalDateTime.now(), ) { companion object { - fun fromJsonArray(jsonString: String, useTagName: Boolean): ArrayList { + fun fromJsonArray( + jsonString: String, useTagName: Boolean, sortMode: SortMode + ): ArrayList { val mapper = jacksonObjectMapper() try { @@ -256,39 +264,55 @@ class DriverFetcherFragment : Fragment() { } releases.add(release) + + println(release.publishTime) } } + when (sortMode) { + SortMode.PublishTime -> releases.sortByDescending { + it.publishTime + } + + else -> {} + } + return releases } catch (e: Exception) { e.printStackTrace() - return ArrayList() + return ArrayList() } } - fun fromJson(node: JsonNode, useTagName: Boolean): Release { + private fun fromJson(node: JsonNode, useTagName: Boolean): Release { try { val tagName = node.get("tag_name").toString().removeSurrounding("\"") val body = node.get("body").toString().removeSurrounding("\"") val prerelease = node.get("prerelease").toString().toBoolean() - val title = if (useTagName) tagName else node.get("name").toString().removeSurrounding("\"") + val titleName = node.get("name").toString().removeSurrounding("\"") + + val published = node.get("published_at").toString().removeSurrounding("\"") + val instantTime: Instant? = Instant.parse(published) + val localTime = instantTime?.atZone(ZoneId.systemDefault())?.toLocalDateTime() ?: LocalDateTime.now() + + val title = if (useTagName) tagName else titleName val assets = node.get("assets") val artifacts = ArrayList() if (assets?.isArray == true) { - assets.forEach { node -> - val urlStr = - node.get("browser_download_url").toString().removeSurrounding("\"") + assets.forEach { subNode -> + val urlStr = subNode.get("browser_download_url").toString() + .removeSurrounding("\"") val url = URL(urlStr) - val name = node.get("name").toString().removeSurrounding("\"") + val name = subNode.get("name").toString().removeSurrounding("\"") val artifact = Artifact(url, name) artifacts.add(artifact) } } - return Release(tagName, title, body, artifacts, prerelease) + return Release(tagName, titleName, title, body, artifacts, prerelease, false, localTime) } catch (e: Exception) { // TODO: handle malformed input. e.printStackTrace() 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 3a9c5486a8..5177e78874 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 @@ -335,7 +335,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { Toast.LENGTH_SHORT ).show() homeViewModel.setCheckKeys(true) - homeViewModel.setCheckFirmware(true) + + val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext) + .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) + if (!firstTimeSetup) { + homeViewModel.setCheckFirmware(true) + } + gamesViewModel.reloadGames(true) return true } else {