mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-23 07:56:52 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac0651a420 | |||
| 5b86242313 | |||
| d24e79c991 | |||
| df05d3de23 | |||
| c249ff462b | |||
| 056d11027c | |||
| 61ffb309a3 | |||
| 65a3cfd2be | |||
| 5c54abf353 |
+7
-3
@@ -99,7 +99,12 @@ class SettingsFragmentPresenter(
|
||||
|
||||
add(BooleanSetting.RENDERER_FRAME_GEN.key)
|
||||
add(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
|
||||
add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
|
||||
if (IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.getInt(
|
||||
getNeedsGlobalForKey(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
|
||||
) == 0
|
||||
) {
|
||||
add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
|
||||
}
|
||||
add(IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key)
|
||||
add(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
|
||||
if (!BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(
|
||||
@@ -307,8 +312,6 @@ class SettingsFragmentPresenter(
|
||||
// TODO(crueter): sub-submenus?
|
||||
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
|
||||
sl.apply {
|
||||
// add(IntSetting.RENDERER_NVDEC_EMULATION.key)
|
||||
|
||||
add(IntSetting.RENDERER_RESOLUTION.key)
|
||||
add(IntSetting.RENDERER_VSYNC.key)
|
||||
add(IntSetting.RENDERER_SCALING_FILTER.key)
|
||||
@@ -325,6 +328,7 @@ class SettingsFragmentPresenter(
|
||||
add(IntSetting.MAX_ANISOTROPY.key)
|
||||
add(IntSetting.RENDERER_VRAM_USAGE_MODE.key)
|
||||
add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key)
|
||||
add(IntSetting.RENDERER_NVDEC_EMULATION.key)
|
||||
|
||||
add(BooleanSetting.SYNC_MEMORY_OPERATIONS.key)
|
||||
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -59,6 +60,10 @@ class GamesFragment : Fragment() {
|
||||
|
||||
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
|
||||
private var fallbackBottomInset: Int = 0
|
||||
private var pendingPostReloadListSettle = false
|
||||
private var pendingPostReloadListSettleGeneration = 0
|
||||
private var gameListSubmitGeneration = 0
|
||||
private var committedGameListSubmitGeneration = 0
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_TEXT = "SearchText"
|
||||
@@ -168,10 +173,9 @@ class GamesFragment : Fragment() {
|
||||
|
||||
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
|
||||
if (shouldScroll) {
|
||||
binding.gridGames.post {
|
||||
(binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true
|
||||
gameAdapter.notifyDataSetChanged()
|
||||
}
|
||||
pendingPostReloadListSettle = true
|
||||
pendingPostReloadListSettleGeneration = gameListSubmitGeneration
|
||||
schedulePostReloadListSettle()
|
||||
gamesViewModel.setShouldScrollAfterReload(false)
|
||||
}
|
||||
}
|
||||
@@ -273,11 +277,42 @@ class GamesFragment : Fragment() {
|
||||
lastSearchText = currentSearchText
|
||||
lastFilter = currentFilter
|
||||
} else {
|
||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
|
||||
submitGameList(games)
|
||||
gamesViewModel.setFilteredGames(games)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitGameList(games: List<Game>) {
|
||||
val adapter = (binding.gridGames as? RecyclerView)?.adapter as? GameAdapter
|
||||
if (adapter == null) {
|
||||
schedulePostReloadListSettle()
|
||||
return
|
||||
}
|
||||
|
||||
val submitGeneration = ++gameListSubmitGeneration
|
||||
adapter.submitList(games) {
|
||||
if (committedGameListSubmitGeneration < submitGeneration) {
|
||||
committedGameListSubmitGeneration = submitGeneration
|
||||
}
|
||||
schedulePostReloadListSettle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun schedulePostReloadListSettle() {
|
||||
if (!pendingPostReloadListSettle || _binding == null) return
|
||||
|
||||
binding.gridGames.doOnPreDraw {
|
||||
if (!pendingPostReloadListSettle || _binding == null) return@doOnPreDraw
|
||||
if (committedGameListSubmitGeneration < pendingPostReloadListSettleGeneration) {
|
||||
schedulePostReloadListSettle()
|
||||
return@doOnPreDraw
|
||||
}
|
||||
pendingPostReloadListSettle = false
|
||||
|
||||
(binding.gridGames as? CarouselRecyclerView)?.refreshView()
|
||||
}
|
||||
}
|
||||
private fun setupTopView() {
|
||||
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
|
||||
if (text.toString().isNotEmpty()) {
|
||||
@@ -414,9 +449,7 @@ class GamesFragment : Fragment() {
|
||||
|
||||
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
||||
if (searchTerm.isEmpty()) {
|
||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(
|
||||
filteredList
|
||||
)
|
||||
submitGameList(filteredList)
|
||||
gamesViewModel.setFilteredGames(filteredList)
|
||||
return
|
||||
}
|
||||
@@ -432,7 +465,7 @@ class GamesFragment : Fragment() {
|
||||
}
|
||||
}.sortedByDescending { it.score }.map { it.item }
|
||||
|
||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(sortedList)
|
||||
submitGameList(sortedList)
|
||||
gamesViewModel.setFilteredGames(sortedList)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ object GameHelper {
|
||||
|
||||
fun getGames(): List<Game> {
|
||||
val games = mutableListOf<Game>()
|
||||
val gamesByProgramId = mutableMapOf<String, Game>()
|
||||
val context = YuzuApplication.appContext
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
@@ -63,6 +64,7 @@ object GameHelper {
|
||||
|
||||
addGamesRecursive(
|
||||
games,
|
||||
gamesByProgramId,
|
||||
FileUtil.listFiles(gameDirUri),
|
||||
scanDepth,
|
||||
mountedContainerUris
|
||||
@@ -136,6 +138,7 @@ object GameHelper {
|
||||
|
||||
private fun addGamesRecursive(
|
||||
games: MutableList<Game>,
|
||||
gamesByProgramId: MutableMap<String, Game>,
|
||||
files: Array<MinimalDocumentFile>,
|
||||
depth: Int,
|
||||
mountedContainerUris: MutableSet<String>
|
||||
@@ -148,6 +151,7 @@ object GameHelper {
|
||||
if (it.isDirectory) {
|
||||
addGamesRecursive(
|
||||
games,
|
||||
gamesByProgramId,
|
||||
FileUtil.listFiles(it.uri),
|
||||
depth - 1,
|
||||
mountedContainerUris
|
||||
@@ -156,8 +160,9 @@ object GameHelper {
|
||||
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||
val filePath = it.uri.toString()
|
||||
|
||||
if (externalContentExtensions.contains(extension) &&
|
||||
mountedContainerUris.add(filePath)) {
|
||||
val mountedContainer = externalContentExtensions.contains(extension) &&
|
||||
mountedContainerUris.add(filePath)
|
||||
if (mountedContainer) {
|
||||
NativeLibrary.addGameFolderFileToFilesystemProvider(filePath)
|
||||
}
|
||||
|
||||
@@ -165,6 +170,20 @@ object GameHelper {
|
||||
val game = getGame(it.uri, true, false)
|
||||
if (game != null) {
|
||||
games.add(game)
|
||||
if (game.programId != "0") {
|
||||
gamesByProgramId[game.programId] = game
|
||||
}
|
||||
} else if (mountedContainer) {
|
||||
GameMetadata.getProgramId(filePath).toLongOrNull()?.let { programId ->
|
||||
gamesByProgramId[(programId and 0x800L.inv()).toString()]
|
||||
}?.let { existingGame ->
|
||||
NativeLibrary.getPatchesForFile(existingGame.path, existingGame.programId)
|
||||
existingGame.version = GameMetadata.getVersion(
|
||||
existingGame.path,
|
||||
true
|
||||
)
|
||||
GameIconUtils.refreshGameIcon(existingGame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -24,6 +27,15 @@ import coil.request.Options
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import java.util.Collections
|
||||
import java.util.WeakHashMap
|
||||
|
||||
private val gameIconHashes = Collections.synchronizedMap(mutableMapOf<String, Int>())
|
||||
private val gameIconTargets = Collections.synchronizedMap(WeakHashMap<ImageView, GameIconTarget>())
|
||||
|
||||
private fun Game.iconCacheKey(): String = "$path|$version"
|
||||
|
||||
private data class GameIconTarget(val game: Game, var iconHash: Int? = null)
|
||||
|
||||
class GameIconFetcher(
|
||||
private val game: Game,
|
||||
@@ -31,14 +43,15 @@ class GameIconFetcher(
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult {
|
||||
return DrawableResult(
|
||||
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
|
||||
drawable = decodeGameIcon(game)!!.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeGameIcon(uri: String): Bitmap? {
|
||||
val data = GameMetadata.getIcon(uri)
|
||||
private fun decodeGameIcon(game: Game): Bitmap? {
|
||||
val data = GameMetadata.getIcon(game.path)
|
||||
gameIconHashes[game.iconCacheKey()] = data.contentHashCode()
|
||||
return BitmapFactory.decodeByteArray(
|
||||
data,
|
||||
0,
|
||||
@@ -54,7 +67,7 @@ class GameIconFetcher(
|
||||
}
|
||||
|
||||
class GameIconKeyer : Keyer<Game> {
|
||||
override fun key(data: Game, options: Options): String = data.path
|
||||
override fun key(data: Game, options: Options): String = data.iconCacheKey()
|
||||
}
|
||||
|
||||
object GameIconUtils {
|
||||
@@ -71,14 +84,58 @@ object GameIconUtils {
|
||||
.build()
|
||||
|
||||
fun loadGameIcon(game: Game, imageView: ImageView) {
|
||||
gameIconTargets[imageView] = GameIconTarget(game)
|
||||
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
||||
.data(game)
|
||||
.target(imageView)
|
||||
.error(R.drawable.default_icon)
|
||||
.listener(
|
||||
onSuccess = { _, _ ->
|
||||
val target = gameIconTargets[imageView]
|
||||
if (target?.game?.iconCacheKey() == game.iconCacheKey()) {
|
||||
gameIconHashes[game.iconCacheKey()]?.let {
|
||||
target.iconHash = it
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = { _, _ ->
|
||||
gameIconTargets[imageView]?.iconHash = null
|
||||
}
|
||||
)
|
||||
.build()
|
||||
imageLoader.enqueue(request)
|
||||
}
|
||||
|
||||
fun refreshGameIcon(game: Game) {
|
||||
val targets = synchronized(gameIconTargets) {
|
||||
gameIconTargets
|
||||
.filterValues { it.game.path == game.path && it.game.programId == game.programId }
|
||||
.keys
|
||||
.toList()
|
||||
}
|
||||
if (targets.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val iconHash = GameMetadata.getIcon(game.path).contentHashCode()
|
||||
val targetsToRefresh = targets.filter { gameIconTargets[it]?.iconHash != iconHash }
|
||||
if (targetsToRefresh.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
imageLoader.memoryCache?.remove(MemoryCache.Key(game.iconCacheKey()))
|
||||
targetsToRefresh.forEach { imageView ->
|
||||
imageView.post {
|
||||
val target = gameIconTargets[imageView] ?: return@post
|
||||
if (target.game.path == game.path && target.game.programId == game.programId) {
|
||||
if (target.iconHash != iconHash) {
|
||||
loadGameIcon(game, imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGameIcon(lifecycleOwner: LifecycleOwner, game: Game): Bitmap {
|
||||
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
||||
.data(game)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.ui
|
||||
@@ -11,6 +11,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||
import androidx.core.view.doOnNextLayout
|
||||
@@ -34,6 +36,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
||||
private var overlapDecoration: OverlappingDecoration? = null
|
||||
private var pagerSnapHelper: PagerSnapHelper? = null
|
||||
private var scalingScrollListener: OnScrollListener? = null
|
||||
private var savedItemAnimator: RecyclerView.ItemAnimator? = null
|
||||
|
||||
companion object {
|
||||
private const val CAROUSEL_CARD_SIZE_FACTOR = "CarouselCardSizeMultiplier"
|
||||
@@ -42,8 +45,13 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
||||
private const val CAROUSEL_OVERLAP_FACTOR = "CarouselOverlapFactor"
|
||||
private const val CAROUSEL_MAX_FLING_COUNT = "CarouselMaxFlingCount"
|
||||
private const val CAROUSEL_FLING_MULTIPLIER = "CarouselFlingMultiplier"
|
||||
private const val CAROUSEL_CARDS_SCALING_SHAPE = "CarouselCardsScalingShape"
|
||||
private const val CAROUSEL_CARDS_ALPHA_SHAPE = "CarouselCardsAlphaShape"
|
||||
private const val CAROUSEL_ARC_ANGLE_STEP_DEGREES = 15.0
|
||||
private const val CAROUSEL_ARC_MAX_ANGLE_DEGREES = 165.0
|
||||
private const val CAROUSEL_ARC_DEPTH_MAX_ANGLE_DEGREES = 85.0
|
||||
private const val CAROUSEL_ARC_DEPTH_STRETCH = 5.0f
|
||||
private const val CAROUSEL_ARC_X_DEPTH_FACTOR = 0.55f
|
||||
private const val CAROUSEL_ARC_FADE_OUT_START_DEGREES = 60.0
|
||||
private const val CAROUSEL_ARC_FADE_OUT_END_DEGREES = 95.0
|
||||
const val CAROUSEL_LAST_SCROLL_POSITION = "CarouselLastScrollPosition"
|
||||
const val CAROUSEL_VIEW_TYPE_PORTRAIT = "GamesViewTypePortrait"
|
||||
const val CAROUSEL_VIEW_TYPE_LANDSCAPE = "GamesViewTypeLandscape"
|
||||
@@ -160,46 +168,52 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun shapingFunction(x: Float, option: Int = 0): Float {
|
||||
return when (option) {
|
||||
0 -> 1f // Off
|
||||
1 -> 1f - x // linear descending
|
||||
2 -> (1f - x) * (1f - x) // Ease out
|
||||
3 -> if (x < 0.05f) 1f else (1f - x) * 0.8f
|
||||
4 -> kotlin.math.cos(x * Math.PI).toFloat() // Cosine
|
||||
5 -> kotlin.math.cos((1.5f * x).coerceIn(0f, 1f) * Math.PI).toFloat() // Cosine 1.5x trimmed
|
||||
else -> 1f // Default to Off
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChildScaleAndAlphaForPosition(child: View) {
|
||||
val cardSize = (adapter as? GameAdapter ?: return).cardSize
|
||||
val position = getChildViewHolder(child).bindingAdapterPosition
|
||||
if (position == RecyclerView.NO_POSITION || cardSize <= 0) {
|
||||
return // No valid position or card size
|
||||
}
|
||||
child.layoutParams.width = cardSize
|
||||
child.layoutParams.height = cardSize
|
||||
val layoutParams = child.layoutParams
|
||||
if (layoutParams.width != cardSize || layoutParams.height != cardSize) {
|
||||
child.layoutParams = layoutParams.apply {
|
||||
width = cardSize
|
||||
height = cardSize
|
||||
}
|
||||
}
|
||||
|
||||
val signedDistance = getChildDistanceToCenter(child)
|
||||
val itemStep = (cardSize - overlapPx).toFloat().coerceAtLeast(1f)
|
||||
val angleStep = Math.toRadians(CAROUSEL_ARC_ANGLE_STEP_DEGREES).toFloat()
|
||||
val maxAngle = Math.toRadians(CAROUSEL_ARC_MAX_ANGLE_DEGREES).toFloat()
|
||||
val depthMaxAngle = Math.toRadians(CAROUSEL_ARC_DEPTH_MAX_ANGLE_DEGREES).toFloat()
|
||||
val fadeOutStartAngle = Math.toRadians(CAROUSEL_ARC_FADE_OUT_START_DEGREES).toFloat()
|
||||
val fadeOutEndAngle = Math.toRadians(CAROUSEL_ARC_FADE_OUT_END_DEGREES).toFloat()
|
||||
val angle = (signedDistance / itemStep * angleStep).coerceIn(-maxAngle, maxAngle)
|
||||
val arcRadius = itemStep / angleStep
|
||||
val arcX = sin(angle) * arcRadius
|
||||
val absoluteAngle = abs(angle)
|
||||
val rawDepthInput = ((1f - cos(absoluteAngle)) / (1f - cos(depthMaxAngle)))
|
||||
.coerceIn(0f, 1f)
|
||||
val easedDepthTail = Math.pow(
|
||||
(1f - rawDepthInput).toDouble(),
|
||||
CAROUSEL_ARC_DEPTH_STRETCH.toDouble()
|
||||
).toFloat()
|
||||
val depthInput = (1f - easedDepthTail).coerceIn(0f, 1f)
|
||||
val projectedArcX = arcX * (1f - rawDepthInput * CAROUSEL_ARC_X_DEPTH_FACTOR)
|
||||
|
||||
child.animate().cancel()
|
||||
child.translationX = projectedArcX - signedDistance
|
||||
|
||||
val center = getRecyclerViewCenter()
|
||||
val distance = abs(getChildDistanceToCenter(child))
|
||||
val internalBorderScale = resources.getFraction(R.fraction.carousel_bordercards_scale, 1, 1)
|
||||
val borderScale = preferences.getFloat(CAROUSEL_BORDERCARDS_SCALE, internalBorderScale).coerceIn(
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
|
||||
val shapeInput = (distance / center).coerceIn(0f, 1f)
|
||||
val internalShapeSetting = resources.getInteger(R.integer.carousel_cards_scaling_shape)
|
||||
val scalingShapeSetting = preferences.getInt(
|
||||
CAROUSEL_CARDS_SCALING_SHAPE,
|
||||
internalShapeSetting
|
||||
)
|
||||
val shapedScaling = shapingFunction(shapeInput, scalingShapeSetting)
|
||||
val shapedScaling = 1f - depthInput
|
||||
val scale = (borderScale + (1f - borderScale) * shapedScaling).coerceIn(0f, 1f)
|
||||
|
||||
val maxDistance = width / 2f
|
||||
val alphaInput = (distance / maxDistance).coerceIn(0f, 1f)
|
||||
val internalBordersAlpha = resources.getFraction(
|
||||
R.fraction.carousel_bordercards_alpha,
|
||||
1,
|
||||
@@ -209,15 +223,12 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
val internalAlphaShapeSetting = resources.getInteger(R.integer.carousel_cards_alpha_shape)
|
||||
val alphaShapeSetting = preferences.getInt(
|
||||
CAROUSEL_CARDS_ALPHA_SHAPE,
|
||||
internalAlphaShapeSetting
|
||||
)
|
||||
val shapedAlpha = shapingFunction(alphaInput, alphaShapeSetting)
|
||||
val alpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f)
|
||||
val shapedAlpha = cos(depthInput * Math.PI).toFloat()
|
||||
val baseAlpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f)
|
||||
val rearPresence = (1f - (absoluteAngle - fadeOutStartAngle) /
|
||||
(fadeOutEndAngle - fadeOutStartAngle)).coerceIn(0f, 1f)
|
||||
val alpha = (baseAlpha * rearPresence).coerceIn(0f, 1f)
|
||||
|
||||
child.animate().cancel()
|
||||
child.alpha = alpha
|
||||
child.scaleX = scale
|
||||
child.scaleY = scale
|
||||
@@ -273,7 +284,9 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
return (userFactor * (height - bottomInset)).toInt()
|
||||
val scaledHeight = height * userFactor
|
||||
val availableHeight = height - bottomInset
|
||||
return minOf(scaledHeight.toInt(), availableHeight.toInt())
|
||||
}
|
||||
|
||||
fun setupCarousel(enabled: Boolean) {
|
||||
@@ -282,6 +295,13 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
||||
if (gameAdapter.cardSize == 0) return
|
||||
if (bottomInset < 0) return
|
||||
|
||||
itemAnimator?.let {
|
||||
if (savedItemAnimator == null) {
|
||||
savedItemAnimator = it
|
||||
}
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
useCustomDrawingOrder = true
|
||||
val cardSize = gameAdapter.cardSize
|
||||
|
||||
@@ -336,6 +356,12 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
||||
// Detach PagerSnapHelper
|
||||
pagerSnapHelper?.attachToRecyclerView(null)
|
||||
pagerSnapHelper = null
|
||||
savedItemAnimator?.let {
|
||||
if (itemAnimator == null) {
|
||||
itemAnimator = it
|
||||
}
|
||||
savedItemAnimator = null
|
||||
}
|
||||
useCustomDrawingOrder = false
|
||||
// Reset padding and fling
|
||||
setPadding(0, 0, 0, 0)
|
||||
@@ -344,6 +370,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
||||
// Reset scaling
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
child?.translationX = 0f
|
||||
child?.scaleX = 1f
|
||||
child?.scaleY = 1f
|
||||
child?.alpha = 1f
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true" android:color="?attr/colorControlHighlight" />
|
||||
<item android:state_focused="true" android:color="@android:color/transparent" />
|
||||
<item android:state_selected="true" android:color="@android:color/transparent" />
|
||||
<item android:state_hovered="true" android:color="@android:color/transparent" />
|
||||
<item android:color="@android:color/transparent" />
|
||||
</selector>
|
||||
@@ -10,6 +10,7 @@
|
||||
android:clipChildren="true"
|
||||
android:layout_margin="0dp"
|
||||
app:cardBackgroundColor="@color/eden_card_background"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/eden_border">
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardPreventCornerOverlap="true"
|
||||
android:clipChildren="true"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
android:layout_margin="4dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
android:focusable="true"
|
||||
android:transitionName="card_game"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
android:foreground="@color/eden_border_gradient_start">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
android:focusable="true"
|
||||
android:transitionName="card_game_compact"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
android:foreground="@color/eden_border_gradient_start">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:cardBackgroundColor="@android:color/transparent"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
<integer name="game_columns_grid">2</integer>
|
||||
<integer name="carousel_max_fling_count">4</integer>
|
||||
<integer name="carousel_focus_search_repeat_threshold_ms">100</integer>
|
||||
<integer name="carousel_cards_scaling_shape">1</integer>
|
||||
<integer name="carousel_cards_alpha_shape">4</integer>
|
||||
|
||||
<!-- Default SWITCH landscape layout -->
|
||||
<integer name="BUTTON_A_X">760</integer>
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
|
||||
<!-- NVDEC Emulation -->
|
||||
<string name="nvdec_emulation">NVDEC Emulation</string>
|
||||
<string name="nvdec_emulation_description">Select how video decoding (NVDEC) is handled during cutscenes and intros.</string>
|
||||
<string name="nvdec_emulation_description">Change to CPU if a crash occurs on cinematics.</string>
|
||||
<string name="nvdec_emulation_cpu" translatable="false">CPU</string>
|
||||
<string name="nvdec_emulation_gpu" translatable="false">GPU</string>
|
||||
<string name="nvdec_emulation_none">None</string>
|
||||
|
||||
@@ -241,7 +241,7 @@ std::optional<std::string> MakeRequest(const std::string& url, const std::string
|
||||
response.status);
|
||||
return {};
|
||||
}
|
||||
if (!response.headers.contains("content-type")) {
|
||||
if (!response.has_header("content-type")) {
|
||||
LOG_ERROR(Common, "GET to {}{} returned no content", url, path);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -5,33 +5,136 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/core.h"
|
||||
#include "core/hle/kernel/k_event.h"
|
||||
#include "core/hle/service/cmif_serialization.h"
|
||||
#include "core/hle/service/cmif_types.h"
|
||||
#include "core/hle/service/ipc_helpers.h"
|
||||
#include "core/hle/service/kernel_helpers.h"
|
||||
#include "core/hle/service/nim/nim.h"
|
||||
#include "core/hle/service/os/event.h"
|
||||
#include "core/hle/service/server_manager.h"
|
||||
#include "core/hle/service/service.h"
|
||||
|
||||
namespace Service::NIM {
|
||||
|
||||
class IShopServiceAsync final : public ServiceFramework<IShopServiceAsync> {
|
||||
public:
|
||||
explicit IShopServiceAsync(Core::System& system_)
|
||||
: ServiceFramework{system_, "IShopServiceAsync"} {
|
||||
: ServiceFramework{system_, "IShopServiceAsync"},
|
||||
service_context{system_, "IShopServiceAsync"} {
|
||||
// clang-format off
|
||||
static const FunctionInfo functions[] = {
|
||||
{0, nullptr, "Cancel"},
|
||||
{1, nullptr, "GetSize"},
|
||||
{2, nullptr, "Read"},
|
||||
{3, nullptr, "GetErrorCode"},
|
||||
{4, nullptr, "Request"},
|
||||
{5, nullptr, "Prepare"},
|
||||
{0, D<&IShopServiceAsync::Cancel>, "Cancel"},
|
||||
{1, D<&IShopServiceAsync::GetSize>, "GetSize"},
|
||||
{2, D<&IShopServiceAsync::Read>, "Read"},
|
||||
{3, D<&IShopServiceAsync::GetErrorCode>, "GetErrorCode"},
|
||||
{4, D<&IShopServiceAsync::Request>, "Request"},
|
||||
{5, D<&IShopServiceAsync::Prepare>, "Prepare"},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
RegisterHandlers(functions);
|
||||
|
||||
completion_event = service_context.CreateEvent("IShopServiceAsync:Completion");
|
||||
}
|
||||
|
||||
~IShopServiceAsync() override {
|
||||
CancelImpl();
|
||||
service_context.CloseEvent(completion_event);
|
||||
}
|
||||
|
||||
Kernel::KReadableEvent* GetEvent() const {
|
||||
return &completion_event->GetReadableEvent();
|
||||
}
|
||||
|
||||
private:
|
||||
KernelHelpers::ServiceContext service_context;
|
||||
Kernel::KEvent* completion_event;
|
||||
|
||||
std::jthread worker;
|
||||
std::atomic<u32> error_code{0};
|
||||
|
||||
std::mutex data_mutex;
|
||||
std::vector<u8> download_data;
|
||||
|
||||
void CancelImpl() {
|
||||
worker.request_stop();
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
Result Cancel() {
|
||||
LOG_DEBUG(Service_NIM, "called");
|
||||
CancelImpl();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetSize(Out<u64> out_size) {
|
||||
LOG_DEBUG(Service_NIM, "called");
|
||||
std::scoped_lock lock{data_mutex};
|
||||
*out_size = download_data.size();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Read(Out<u64> out_size, u64 offset, OutBuffer<BufferAttr_HipcAutoSelect> out_buffer) {
|
||||
std::scoped_lock lock{data_mutex};
|
||||
|
||||
u64 actual_read = 0;
|
||||
if (offset < download_data.size()) {
|
||||
actual_read = std::min<u64>(out_buffer.size(), download_data.size() - offset);
|
||||
std::memcpy(out_buffer.data(), download_data.data() + offset, actual_read);
|
||||
}
|
||||
|
||||
*out_size = actual_read;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetErrorCode(Out<u32> out_error_code) {
|
||||
LOG_DEBUG(Service_NIM, "called");
|
||||
*out_error_code = error_code.load();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Request() {
|
||||
LOG_DEBUG(Service_NIM, "(STUBBED) called");
|
||||
CancelImpl();
|
||||
|
||||
error_code.store(0);
|
||||
completion_event->Clear(system.Kernel());
|
||||
|
||||
{
|
||||
std::scoped_lock lock{data_mutex};
|
||||
download_data.clear();
|
||||
}
|
||||
|
||||
worker = std::jthread([this](const std::stop_token& stop_token) {
|
||||
if (stop_token.stop_requested()) {
|
||||
error_code.store(1);
|
||||
} else {
|
||||
std::scoped_lock lock{data_mutex};
|
||||
// Dummy JSON response, else it fails...
|
||||
const std::string dummy_response = "{}";
|
||||
download_data.assign(dummy_response.begin(), dummy_response.end());
|
||||
error_code.store(0);
|
||||
}
|
||||
completion_event->Signal(system.Kernel());
|
||||
});
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Prepare(InArray<char, BufferAttr_HipcMapAlias> in_path, InArray<char, BufferAttr_HipcMapAlias> in_post) {
|
||||
LOG_DEBUG(Service_NIM, "called");
|
||||
if (!in_path.empty()) {
|
||||
std::string url(in_path.data(), in_path.size());
|
||||
LOG_INFO(Service_NIM, "Preparing request for URL: {}", url);
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,11 +152,13 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void CreateAsyncInterface(HLERequestContext& ctx) {
|
||||
LOG_WARNING(Service_NIM, "(STUBBED) called");
|
||||
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
||||
void CreateAsyncInterface(HLERequestContext& ctx) {LOG_DEBUG(Service_NIM, "called");
|
||||
auto async_interface = std::make_shared<IShopServiceAsync>(system);
|
||||
|
||||
IPC::ResponseBuilder rb{ctx, 2, 1, 1};
|
||||
rb.Push(ResultSuccess);
|
||||
rb.PushIpcInterface<IShopServiceAsync>(ctx, system);
|
||||
rb.PushCopyObjects(ctx, async_interface->GetEvent());
|
||||
rb.PushIpcInterface<IShopServiceAsync>(ctx, std::move(async_interface));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -114,13 +114,13 @@ std::vector<Network::ScanData> ScanWifiNetworks(std::chrono::milliseconds deadli
|
||||
char ifname[IFNAMSIZ] = {0};
|
||||
char *args[1] = {ifname};
|
||||
|
||||
iw_enum_devices(sock, [](int skfd, char* ifname, char* args[], int count) -> int {
|
||||
iw_enum_devices(sock, [](int f_skfd, char* f_ifname, char* f_args[], int) -> int {
|
||||
iwrange range;
|
||||
int res = iw_get_range_info(skfd, ifname, &range);
|
||||
LOG_INFO(Network, "ifname {} returned {} on iw_get_range_info", ifname, res);
|
||||
int res = iw_get_range_info(f_skfd, f_ifname, &range);
|
||||
LOG_INFO(Network, "ifname {} returned {} on iw_get_range_info", f_ifname, res);
|
||||
if (res >= 0) {
|
||||
strncpy(args[0], ifname, IFNAMSIZ - 1);
|
||||
args[0][IFNAMSIZ - 1] = 0;
|
||||
strncpy(f_args[0], f_ifname, IFNAMSIZ - 1);
|
||||
f_args[0][IFNAMSIZ - 1] = 0;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <variant>
|
||||
#include "dynarmic/backend/loongarch64/emit_loongarch64.h"
|
||||
|
||||
#include "dynarmic/backend/loongarch64/a32_jitstate.h"
|
||||
@@ -95,7 +96,8 @@ EmittedBlockInfo EmitLoongArch64(lagoon_assembler_t& as, IR::Block block, const
|
||||
|
||||
// TODO: Emit Terminal
|
||||
const auto term = block.GetTerminal();
|
||||
const IR::Term::LinkBlock* link_block_term = boost::get<IR::Term::LinkBlock>(&term);
|
||||
const IR::Term::LeafTerminal* leaft_term = std::get_if<IR::Term::LeafTerminal>(&term);
|
||||
const IR::Term::LinkBlock* link_block_term = std::get_if<IR::Term::LinkBlock>(leaft_term);
|
||||
ASSERT(link_block_term);
|
||||
la_load_immediate64(&as, Xscratch0, link_block_term->next.Value());
|
||||
la_st_w(&as, Xscratch0, Xstate, static_cast<int32_t>(offsetof(A32JitState, regs) + sizeof(u32) * 15));
|
||||
|
||||
@@ -74,6 +74,7 @@ UpdateDialog::~UpdateDialog() {
|
||||
delete ui;
|
||||
}
|
||||
|
||||
// TODO: migrate to a net.cpp wrapper
|
||||
void UpdateDialog::Download() {
|
||||
const auto filename = QtCommon::Frontend::GetSaveFileName(
|
||||
tr("New Version Location"),
|
||||
@@ -161,7 +162,7 @@ void UpdateDialog::Download() {
|
||||
QString::number(response.status)));
|
||||
return;
|
||||
}
|
||||
if (!response.headers.contains("content-type")) {
|
||||
if (!response.has_header("content-type")) {
|
||||
LOG_ERROR(Frontend, "GET to {}{} returned no content", m_asset.url, m_asset.path);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user