mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-10-31 11:40:52 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b66200df0f | |||
| 5b86242313 | |||
| d24e79c991 | |||
| df05d3de23 | |||
| c249ff462b | |||
| 056d11027c | |||
| 61ffb309a3 | |||
| 65a3cfd2be | |||
| 5c54abf353 |
+7
-3
@@ -99,7 +99,12 @@ class SettingsFragmentPresenter(
|
|||||||
|
|
||||||
add(BooleanSetting.RENDERER_FRAME_GEN.key)
|
add(BooleanSetting.RENDERER_FRAME_GEN.key)
|
||||||
add(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.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(IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key)
|
||||||
add(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
|
add(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
|
||||||
if (!BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(
|
if (!BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(
|
||||||
@@ -307,8 +312,6 @@ class SettingsFragmentPresenter(
|
|||||||
// TODO(crueter): sub-submenus?
|
// TODO(crueter): sub-submenus?
|
||||||
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
|
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
|
||||||
sl.apply {
|
sl.apply {
|
||||||
// add(IntSetting.RENDERER_NVDEC_EMULATION.key)
|
|
||||||
|
|
||||||
add(IntSetting.RENDERER_RESOLUTION.key)
|
add(IntSetting.RENDERER_RESOLUTION.key)
|
||||||
add(IntSetting.RENDERER_VSYNC.key)
|
add(IntSetting.RENDERER_VSYNC.key)
|
||||||
add(IntSetting.RENDERER_SCALING_FILTER.key)
|
add(IntSetting.RENDERER_SCALING_FILTER.key)
|
||||||
@@ -325,6 +328,7 @@ class SettingsFragmentPresenter(
|
|||||||
add(IntSetting.MAX_ANISOTROPY.key)
|
add(IntSetting.MAX_ANISOTROPY.key)
|
||||||
add(IntSetting.RENDERER_VRAM_USAGE_MODE.key)
|
add(IntSetting.RENDERER_VRAM_USAGE_MODE.key)
|
||||||
add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key)
|
add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key)
|
||||||
|
add(IntSetting.RENDERER_NVDEC_EMULATION.key)
|
||||||
|
|
||||||
add(BooleanSetting.SYNC_MEMORY_OPERATIONS.key)
|
add(BooleanSetting.SYNC_MEMORY_OPERATIONS.key)
|
||||||
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.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.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.doOnPreDraw
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@@ -59,6 +60,10 @@ class GamesFragment : Fragment() {
|
|||||||
|
|
||||||
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
|
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
|
||||||
private var fallbackBottomInset: Int = 0
|
private var fallbackBottomInset: Int = 0
|
||||||
|
private var pendingPostReloadListSettle = false
|
||||||
|
private var pendingPostReloadListSettleGeneration = 0
|
||||||
|
private var gameListSubmitGeneration = 0
|
||||||
|
private var committedGameListSubmitGeneration = 0
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SEARCH_TEXT = "SearchText"
|
private const val SEARCH_TEXT = "SearchText"
|
||||||
@@ -168,10 +173,9 @@ class GamesFragment : Fragment() {
|
|||||||
|
|
||||||
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
|
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
|
||||||
if (shouldScroll) {
|
if (shouldScroll) {
|
||||||
binding.gridGames.post {
|
pendingPostReloadListSettle = true
|
||||||
(binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true
|
pendingPostReloadListSettleGeneration = gameListSubmitGeneration
|
||||||
gameAdapter.notifyDataSetChanged()
|
schedulePostReloadListSettle()
|
||||||
}
|
|
||||||
gamesViewModel.setShouldScrollAfterReload(false)
|
gamesViewModel.setShouldScrollAfterReload(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,11 +277,42 @@ class GamesFragment : Fragment() {
|
|||||||
lastSearchText = currentSearchText
|
lastSearchText = currentSearchText
|
||||||
lastFilter = currentFilter
|
lastFilter = currentFilter
|
||||||
} else {
|
} else {
|
||||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
|
submitGameList(games)
|
||||||
gamesViewModel.setFilteredGames(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() {
|
private fun setupTopView() {
|
||||||
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
|
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
|
||||||
if (text.toString().isNotEmpty()) {
|
if (text.toString().isNotEmpty()) {
|
||||||
@@ -414,9 +449,7 @@ class GamesFragment : Fragment() {
|
|||||||
|
|
||||||
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
||||||
if (searchTerm.isEmpty()) {
|
if (searchTerm.isEmpty()) {
|
||||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(
|
submitGameList(filteredList)
|
||||||
filteredList
|
|
||||||
)
|
|
||||||
gamesViewModel.setFilteredGames(filteredList)
|
gamesViewModel.setFilteredGames(filteredList)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -432,7 +465,7 @@ class GamesFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}.sortedByDescending { it.score }.map { it.item }
|
}.sortedByDescending { it.score }.map { it.item }
|
||||||
|
|
||||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(sortedList)
|
submitGameList(sortedList)
|
||||||
gamesViewModel.setFilteredGames(sortedList)
|
gamesViewModel.setFilteredGames(sortedList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ object GameHelper {
|
|||||||
|
|
||||||
fun getGames(): List<Game> {
|
fun getGames(): List<Game> {
|
||||||
val games = mutableListOf<Game>()
|
val games = mutableListOf<Game>()
|
||||||
|
val gamesByProgramId = mutableMapOf<String, Game>()
|
||||||
val context = YuzuApplication.appContext
|
val context = YuzuApplication.appContext
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ object GameHelper {
|
|||||||
|
|
||||||
addGamesRecursive(
|
addGamesRecursive(
|
||||||
games,
|
games,
|
||||||
|
gamesByProgramId,
|
||||||
FileUtil.listFiles(gameDirUri),
|
FileUtil.listFiles(gameDirUri),
|
||||||
scanDepth,
|
scanDepth,
|
||||||
mountedContainerUris
|
mountedContainerUris
|
||||||
@@ -136,6 +138,7 @@ object GameHelper {
|
|||||||
|
|
||||||
private fun addGamesRecursive(
|
private fun addGamesRecursive(
|
||||||
games: MutableList<Game>,
|
games: MutableList<Game>,
|
||||||
|
gamesByProgramId: MutableMap<String, Game>,
|
||||||
files: Array<MinimalDocumentFile>,
|
files: Array<MinimalDocumentFile>,
|
||||||
depth: Int,
|
depth: Int,
|
||||||
mountedContainerUris: MutableSet<String>
|
mountedContainerUris: MutableSet<String>
|
||||||
@@ -148,6 +151,7 @@ object GameHelper {
|
|||||||
if (it.isDirectory) {
|
if (it.isDirectory) {
|
||||||
addGamesRecursive(
|
addGamesRecursive(
|
||||||
games,
|
games,
|
||||||
|
gamesByProgramId,
|
||||||
FileUtil.listFiles(it.uri),
|
FileUtil.listFiles(it.uri),
|
||||||
depth - 1,
|
depth - 1,
|
||||||
mountedContainerUris
|
mountedContainerUris
|
||||||
@@ -156,8 +160,9 @@ object GameHelper {
|
|||||||
val extension = FileUtil.getExtension(it.uri).lowercase()
|
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||||
val filePath = it.uri.toString()
|
val filePath = it.uri.toString()
|
||||||
|
|
||||||
if (externalContentExtensions.contains(extension) &&
|
val mountedContainer = externalContentExtensions.contains(extension) &&
|
||||||
mountedContainerUris.add(filePath)) {
|
mountedContainerUris.add(filePath)
|
||||||
|
if (mountedContainer) {
|
||||||
NativeLibrary.addGameFolderFileToFilesystemProvider(filePath)
|
NativeLibrary.addGameFolderFileToFilesystemProvider(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +170,20 @@ object GameHelper {
|
|||||||
val game = getGame(it.uri, true, false)
|
val game = getGame(it.uri, true, false)
|
||||||
if (game != null) {
|
if (game != null) {
|
||||||
games.add(game)
|
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-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// 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.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
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(
|
class GameIconFetcher(
|
||||||
private val game: Game,
|
private val game: Game,
|
||||||
@@ -31,14 +43,15 @@ class GameIconFetcher(
|
|||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
override suspend fun fetch(): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
return DrawableResult(
|
return DrawableResult(
|
||||||
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
|
drawable = decodeGameIcon(game)!!.toDrawable(options.context.resources),
|
||||||
isSampled = false,
|
isSampled = false,
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeGameIcon(uri: String): Bitmap? {
|
private fun decodeGameIcon(game: Game): Bitmap? {
|
||||||
val data = GameMetadata.getIcon(uri)
|
val data = GameMetadata.getIcon(game.path)
|
||||||
|
gameIconHashes[game.iconCacheKey()] = data.contentHashCode()
|
||||||
return BitmapFactory.decodeByteArray(
|
return BitmapFactory.decodeByteArray(
|
||||||
data,
|
data,
|
||||||
0,
|
0,
|
||||||
@@ -54,7 +67,7 @@ class GameIconFetcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GameIconKeyer : Keyer<Game> {
|
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 {
|
object GameIconUtils {
|
||||||
@@ -71,14 +84,58 @@ object GameIconUtils {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun loadGameIcon(game: Game, imageView: ImageView) {
|
fun loadGameIcon(game: Game, imageView: ImageView) {
|
||||||
|
gameIconTargets[imageView] = GameIconTarget(game)
|
||||||
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
||||||
.data(game)
|
.data(game)
|
||||||
.target(imageView)
|
.target(imageView)
|
||||||
.error(R.drawable.default_icon)
|
.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()
|
.build()
|
||||||
imageLoader.enqueue(request)
|
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 {
|
suspend fun getGameIcon(lifecycleOwner: LifecycleOwner, game: Game): Bitmap {
|
||||||
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
||||||
.data(game)
|
.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
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.ui
|
package org.yuzu.yuzu_emu.ui
|
||||||
@@ -11,6 +11,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
import androidx.recyclerview.widget.PagerSnapHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||||
import androidx.core.view.doOnNextLayout
|
import androidx.core.view.doOnNextLayout
|
||||||
@@ -34,6 +36,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
private var overlapDecoration: OverlappingDecoration? = null
|
private var overlapDecoration: OverlappingDecoration? = null
|
||||||
private var pagerSnapHelper: PagerSnapHelper? = null
|
private var pagerSnapHelper: PagerSnapHelper? = null
|
||||||
private var scalingScrollListener: OnScrollListener? = null
|
private var scalingScrollListener: OnScrollListener? = null
|
||||||
|
private var savedItemAnimator: RecyclerView.ItemAnimator? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CAROUSEL_CARD_SIZE_FACTOR = "CarouselCardSizeMultiplier"
|
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_OVERLAP_FACTOR = "CarouselOverlapFactor"
|
||||||
private const val CAROUSEL_MAX_FLING_COUNT = "CarouselMaxFlingCount"
|
private const val CAROUSEL_MAX_FLING_COUNT = "CarouselMaxFlingCount"
|
||||||
private const val CAROUSEL_FLING_MULTIPLIER = "CarouselFlingMultiplier"
|
private const val CAROUSEL_FLING_MULTIPLIER = "CarouselFlingMultiplier"
|
||||||
private const val CAROUSEL_CARDS_SCALING_SHAPE = "CarouselCardsScalingShape"
|
private const val CAROUSEL_ARC_ANGLE_STEP_DEGREES = 15.0
|
||||||
private const val CAROUSEL_CARDS_ALPHA_SHAPE = "CarouselCardsAlphaShape"
|
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_LAST_SCROLL_POSITION = "CarouselLastScrollPosition"
|
||||||
const val CAROUSEL_VIEW_TYPE_PORTRAIT = "GamesViewTypePortrait"
|
const val CAROUSEL_VIEW_TYPE_PORTRAIT = "GamesViewTypePortrait"
|
||||||
const val CAROUSEL_VIEW_TYPE_LANDSCAPE = "GamesViewTypeLandscape"
|
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) {
|
fun updateChildScaleAndAlphaForPosition(child: View) {
|
||||||
val cardSize = (adapter as? GameAdapter ?: return).cardSize
|
val cardSize = (adapter as? GameAdapter ?: return).cardSize
|
||||||
val position = getChildViewHolder(child).bindingAdapterPosition
|
val position = getChildViewHolder(child).bindingAdapterPosition
|
||||||
if (position == RecyclerView.NO_POSITION || cardSize <= 0) {
|
if (position == RecyclerView.NO_POSITION || cardSize <= 0) {
|
||||||
return // No valid position or card size
|
return // No valid position or card size
|
||||||
}
|
}
|
||||||
child.layoutParams.width = cardSize
|
val layoutParams = child.layoutParams
|
||||||
child.layoutParams.height = cardSize
|
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 internalBorderScale = resources.getFraction(R.fraction.carousel_bordercards_scale, 1, 1)
|
||||||
val borderScale = preferences.getFloat(CAROUSEL_BORDERCARDS_SCALE, internalBorderScale).coerceIn(
|
val borderScale = preferences.getFloat(CAROUSEL_BORDERCARDS_SCALE, internalBorderScale).coerceIn(
|
||||||
0f,
|
0f,
|
||||||
1f
|
1f
|
||||||
)
|
)
|
||||||
|
|
||||||
val shapeInput = (distance / center).coerceIn(0f, 1f)
|
val shapedScaling = 1f - depthInput
|
||||||
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 scale = (borderScale + (1f - borderScale) * shapedScaling).coerceIn(0f, 1f)
|
val scale = (borderScale + (1f - borderScale) * shapedScaling).coerceIn(0f, 1f)
|
||||||
|
|
||||||
val maxDistance = width / 2f
|
|
||||||
val alphaInput = (distance / maxDistance).coerceIn(0f, 1f)
|
|
||||||
val internalBordersAlpha = resources.getFraction(
|
val internalBordersAlpha = resources.getFraction(
|
||||||
R.fraction.carousel_bordercards_alpha,
|
R.fraction.carousel_bordercards_alpha,
|
||||||
1,
|
1,
|
||||||
@@ -209,15 +223,12 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
0f,
|
0f,
|
||||||
1f
|
1f
|
||||||
)
|
)
|
||||||
val internalAlphaShapeSetting = resources.getInteger(R.integer.carousel_cards_alpha_shape)
|
val shapedAlpha = cos(depthInput * Math.PI).toFloat()
|
||||||
val alphaShapeSetting = preferences.getInt(
|
val baseAlpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f)
|
||||||
CAROUSEL_CARDS_ALPHA_SHAPE,
|
val rearPresence = (1f - (absoluteAngle - fadeOutStartAngle) /
|
||||||
internalAlphaShapeSetting
|
(fadeOutEndAngle - fadeOutStartAngle)).coerceIn(0f, 1f)
|
||||||
)
|
val alpha = (baseAlpha * rearPresence).coerceIn(0f, 1f)
|
||||||
val shapedAlpha = shapingFunction(alphaInput, alphaShapeSetting)
|
|
||||||
val alpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f)
|
|
||||||
|
|
||||||
child.animate().cancel()
|
|
||||||
child.alpha = alpha
|
child.alpha = alpha
|
||||||
child.scaleX = scale
|
child.scaleX = scale
|
||||||
child.scaleY = scale
|
child.scaleY = scale
|
||||||
@@ -273,7 +284,9 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
0f,
|
0f,
|
||||||
1f
|
1f
|
||||||
)
|
)
|
||||||
return (userFactor * (height - bottomInset)).toInt()
|
val scaledHeight = height * userFactor
|
||||||
|
val availableHeight = height - bottomInset
|
||||||
|
return minOf(scaledHeight.toInt(), availableHeight.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupCarousel(enabled: Boolean) {
|
fun setupCarousel(enabled: Boolean) {
|
||||||
@@ -282,6 +295,13 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
if (gameAdapter.cardSize == 0) return
|
if (gameAdapter.cardSize == 0) return
|
||||||
if (bottomInset < 0) return
|
if (bottomInset < 0) return
|
||||||
|
|
||||||
|
itemAnimator?.let {
|
||||||
|
if (savedItemAnimator == null) {
|
||||||
|
savedItemAnimator = it
|
||||||
|
}
|
||||||
|
itemAnimator = null
|
||||||
|
}
|
||||||
|
|
||||||
useCustomDrawingOrder = true
|
useCustomDrawingOrder = true
|
||||||
val cardSize = gameAdapter.cardSize
|
val cardSize = gameAdapter.cardSize
|
||||||
|
|
||||||
@@ -336,6 +356,12 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
// Detach PagerSnapHelper
|
// Detach PagerSnapHelper
|
||||||
pagerSnapHelper?.attachToRecyclerView(null)
|
pagerSnapHelper?.attachToRecyclerView(null)
|
||||||
pagerSnapHelper = null
|
pagerSnapHelper = null
|
||||||
|
savedItemAnimator?.let {
|
||||||
|
if (itemAnimator == null) {
|
||||||
|
itemAnimator = it
|
||||||
|
}
|
||||||
|
savedItemAnimator = null
|
||||||
|
}
|
||||||
useCustomDrawingOrder = false
|
useCustomDrawingOrder = false
|
||||||
// Reset padding and fling
|
// Reset padding and fling
|
||||||
setPadding(0, 0, 0, 0)
|
setPadding(0, 0, 0, 0)
|
||||||
@@ -344,6 +370,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
// Reset scaling
|
// Reset scaling
|
||||||
for (i in 0 until childCount) {
|
for (i in 0 until childCount) {
|
||||||
val child = getChildAt(i)
|
val child = getChildAt(i)
|
||||||
|
child?.translationX = 0f
|
||||||
child?.scaleX = 1f
|
child?.scaleX = 1f
|
||||||
child?.scaleY = 1f
|
child?.scaleY = 1f
|
||||||
child?.alpha = 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:clipChildren="true"
|
||||||
android:layout_margin="0dp"
|
android:layout_margin="0dp"
|
||||||
app:cardBackgroundColor="@color/eden_card_background"
|
app:cardBackgroundColor="@color/eden_card_background"
|
||||||
|
app:rippleColor="@color/game_card_ripple"
|
||||||
app:strokeWidth="1dp"
|
app:strokeWidth="1dp"
|
||||||
app:strokeColor="@color/eden_border">
|
app:strokeColor="@color/eden_border">
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
app:cardCornerRadius="16dp"
|
app:cardCornerRadius="16dp"
|
||||||
app:cardPreventCornerOverlap="true"
|
app:cardPreventCornerOverlap="true"
|
||||||
android:clipChildren="true"
|
android:clipChildren="true"
|
||||||
|
app:rippleColor="@color/game_card_ripple"
|
||||||
android:layout_margin="4dp">
|
android:layout_margin="4dp">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:transitionName="card_game"
|
android:transitionName="card_game"
|
||||||
app:cardCornerRadius="16dp"
|
app:cardCornerRadius="16dp"
|
||||||
|
app:rippleColor="@color/game_card_ripple"
|
||||||
android:foreground="@color/eden_border_gradient_start">
|
android:foreground="@color/eden_border_gradient_start">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:transitionName="card_game_compact"
|
android:transitionName="card_game_compact"
|
||||||
app:cardCornerRadius="16dp"
|
app:cardCornerRadius="16dp"
|
||||||
|
app:rippleColor="@color/game_card_ripple"
|
||||||
android:foreground="@color/eden_border_gradient_start">
|
android:foreground="@color/eden_border_gradient_start">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
app:cardCornerRadius="16dp"
|
app:cardCornerRadius="16dp"
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:cardBackgroundColor="@android:color/transparent"
|
app:cardBackgroundColor="@android:color/transparent"
|
||||||
|
app:rippleColor="@color/game_card_ripple"
|
||||||
app:strokeWidth="0dp">
|
app:strokeWidth="0dp">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
<integer name="game_columns_grid">2</integer>
|
<integer name="game_columns_grid">2</integer>
|
||||||
<integer name="carousel_max_fling_count">4</integer>
|
<integer name="carousel_max_fling_count">4</integer>
|
||||||
<integer name="carousel_focus_search_repeat_threshold_ms">100</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 -->
|
<!-- Default SWITCH landscape layout -->
|
||||||
<integer name="BUTTON_A_X">760</integer>
|
<integer name="BUTTON_A_X">760</integer>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
<string name="nvdec_emulation">NVDEC Emulation</string>
|
<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_cpu" translatable="false">CPU</string>
|
||||||
<string name="nvdec_emulation_gpu" translatable="false">GPU</string>
|
<string name="nvdec_emulation_gpu" translatable="false">GPU</string>
|
||||||
<string name="nvdec_emulation_none">None</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);
|
response.status);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (!response.headers.contains("content-type")) {
|
if (!response.has_header("content-type")) {
|
||||||
LOG_ERROR(Common, "GET to {}{} returned no content", url, path);
|
LOG_ERROR(Common, "GET to {}{} returned no content", url, path);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-6
@@ -116,20 +116,122 @@ std::string ReplaceAll(std::string result, const std::string& src, const std::st
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string UTF16ToUTF8(std::u16string_view input) {
|
std::string UTF16ToUTF8(std::u16string_view input) {
|
||||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
|
std::string result;
|
||||||
return convert.to_bytes(input.data(), input.data() + input.size());
|
result.reserve(input.size() * 4);
|
||||||
|
for (size_t i = 0; i < input.size(); ++i) {
|
||||||
|
uint32_t code = input[i];
|
||||||
|
// Handle surrogate pairs
|
||||||
|
if (code >= 0xD800 && code <= 0xDBFF) {
|
||||||
|
if (i + 1 < input.size()) {
|
||||||
|
uint32_t low = input[i + 1];
|
||||||
|
if (low >= 0xDC00 && low <= 0xDFFF) {
|
||||||
|
code = ((code - 0xD800) << 10) + (low - 0xDC00) + 0x10000;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (code <= 0x7F) {
|
||||||
|
result.push_back(char(code));
|
||||||
|
} else if (code <= 0x7FF) {
|
||||||
|
result.push_back(char(0xC0 | (code >> 6)));
|
||||||
|
result.push_back(char(0x80 | (code & 0x3F)));
|
||||||
|
} else if (code <= 0xFFFF) {
|
||||||
|
result.push_back(char(0xE0 | (code >> 12)));
|
||||||
|
result.push_back(char(0x80 | ((code >> 6) & 0x3F)));
|
||||||
|
result.push_back(char(0x80 | (code & 0x3F)));
|
||||||
|
} else {
|
||||||
|
result.push_back(char(0xF0 | (code >> 18)));
|
||||||
|
result.push_back(char(0x80 | ((code >> 12) & 0x3F)));
|
||||||
|
result.push_back(char(0x80 | ((code >> 6) & 0x3F)));
|
||||||
|
result.push_back(char(0x80 | (code & 0x3F)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::u16string UTF8ToUTF16(std::string_view input) {
|
std::u16string UTF8ToUTF16(std::string_view input) {
|
||||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
|
std::u16string result;
|
||||||
return convert.from_bytes(input.data(), input.data() + input.size());
|
result.reserve(input.size() * 2);
|
||||||
|
for (size_t i = 0; i < input.size(); ) {
|
||||||
|
uint32_t code = 0;
|
||||||
|
unsigned char c = input[i];
|
||||||
|
size_t len = 0;
|
||||||
|
if ((c & 0x80) == 0) {
|
||||||
|
code = c;
|
||||||
|
len = 1;
|
||||||
|
} else if ((c & 0xE0) == 0xC0) {
|
||||||
|
code = c & 0x1F;
|
||||||
|
len = 2;
|
||||||
|
} else if ((c & 0xF0) == 0xE0) {
|
||||||
|
code = c & 0x0F;
|
||||||
|
len = 3;
|
||||||
|
} else if ((c & 0xF8) == 0xF0) {
|
||||||
|
code = c & 0x07;
|
||||||
|
len = 4;
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (i + len - 1 >= input.size())
|
||||||
|
break;
|
||||||
|
for (size_t j = 1; j <= len - 1; ++j) {
|
||||||
|
if ((input[i + j] & 0xC0) != 0x80) {
|
||||||
|
code = 0xFFFD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
code = (code << 6) | (input[i + j] & 0x3F);
|
||||||
|
}
|
||||||
|
if (code <= 0xFFFF) {
|
||||||
|
result.push_back(char16_t(code));
|
||||||
|
} else {
|
||||||
|
code -= 0x10000;
|
||||||
|
result.push_back(char16_t(0xD800 + (code >> 10)));
|
||||||
|
result.push_back(char16_t(0xDC00 + (code & 0x3FF)));
|
||||||
|
}
|
||||||
|
i += len;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::u32string UTF8ToUTF32(std::string_view input) {
|
std::u32string UTF8ToUTF32(std::string_view input) {
|
||||||
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> convert;
|
std::u32string result;
|
||||||
return convert.from_bytes(input.data(), input.data() + input.size());
|
result.reserve(input.size());
|
||||||
|
for (size_t i = 0; i < input.size(); ) {
|
||||||
|
uint32_t code = 0;
|
||||||
|
unsigned char c = input[i];
|
||||||
|
size_t len = 0;
|
||||||
|
if ((c & 0x80) == 0) {
|
||||||
|
code = c;
|
||||||
|
len = 1;
|
||||||
|
} else if ((c & 0xE0) == 0xC0) {
|
||||||
|
code = c & 0x1F;
|
||||||
|
len = 2;
|
||||||
|
} else if ((c & 0xF0) == 0xE0) {
|
||||||
|
code = c & 0x0F;
|
||||||
|
len = 3;
|
||||||
|
} else if ((c & 0xF8) == 0xF0) {
|
||||||
|
code = c & 0x07;
|
||||||
|
len = 4;
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (i + len - 1 >= input.size())
|
||||||
|
break;
|
||||||
|
for (size_t j = 1; j <= len - 1; ++j) {
|
||||||
|
if ((input[i + j] & 0xC0) != 0x80) {
|
||||||
|
code = 0xFFFD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
code = (code << 6) | (input[i + j] & 0x3F);
|
||||||
|
}
|
||||||
|
result.push_back(code);
|
||||||
|
i += len;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
static std::wstring CPToUTF16(u32 code_page, std::string_view input) {
|
static std::wstring CPToUTF16(u32 code_page, std::string_view input) {
|
||||||
const auto size =
|
const auto size =
|
||||||
|
|||||||
@@ -5,33 +5,136 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ctime>
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/hle/kernel/k_event.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/ipc_helpers.h"
|
||||||
#include "core/hle/service/kernel_helpers.h"
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
#include "core/hle/service/nim/nim.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/server_manager.h"
|
||||||
#include "core/hle/service/service.h"
|
#include "core/hle/service/service.h"
|
||||||
|
|
||||||
namespace Service::NIM {
|
namespace Service::NIM {
|
||||||
|
|
||||||
class IShopServiceAsync final : public ServiceFramework<IShopServiceAsync> {
|
class IShopServiceAsync final : public ServiceFramework<IShopServiceAsync> {
|
||||||
public:
|
public:
|
||||||
explicit IShopServiceAsync(Core::System& system_)
|
explicit IShopServiceAsync(Core::System& system_)
|
||||||
: ServiceFramework{system_, "IShopServiceAsync"} {
|
: ServiceFramework{system_, "IShopServiceAsync"},
|
||||||
|
service_context{system_, "IShopServiceAsync"} {
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const FunctionInfo functions[] = {
|
static const FunctionInfo functions[] = {
|
||||||
{0, nullptr, "Cancel"},
|
{0, D<&IShopServiceAsync::Cancel>, "Cancel"},
|
||||||
{1, nullptr, "GetSize"},
|
{1, D<&IShopServiceAsync::GetSize>, "GetSize"},
|
||||||
{2, nullptr, "Read"},
|
{2, D<&IShopServiceAsync::Read>, "Read"},
|
||||||
{3, nullptr, "GetErrorCode"},
|
{3, D<&IShopServiceAsync::GetErrorCode>, "GetErrorCode"},
|
||||||
{4, nullptr, "Request"},
|
{4, D<&IShopServiceAsync::Request>, "Request"},
|
||||||
{5, nullptr, "Prepare"},
|
{5, D<&IShopServiceAsync::Prepare>, "Prepare"},
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
RegisterHandlers(functions);
|
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:
|
private:
|
||||||
void CreateAsyncInterface(HLERequestContext& ctx) {
|
void CreateAsyncInterface(HLERequestContext& ctx) {LOG_DEBUG(Service_NIM, "called");
|
||||||
LOG_WARNING(Service_NIM, "(STUBBED) called");
|
auto async_interface = std::make_shared<IShopServiceAsync>(system);
|
||||||
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2, 1, 1};
|
||||||
rb.Push(ResultSuccess);
|
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 ifname[IFNAMSIZ] = {0};
|
||||||
char *args[1] = {ifname};
|
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;
|
iwrange range;
|
||||||
int res = iw_get_range_info(skfd, ifname, &range);
|
int res = iw_get_range_info(f_skfd, f_ifname, &range);
|
||||||
LOG_INFO(Network, "ifname {} returned {} on iw_get_range_info", ifname, res);
|
LOG_INFO(Network, "ifname {} returned {} on iw_get_range_info", f_ifname, res);
|
||||||
if (res >= 0) {
|
if (res >= 0) {
|
||||||
strncpy(args[0], ifname, IFNAMSIZ - 1);
|
strncpy(f_args[0], f_ifname, IFNAMSIZ - 1);
|
||||||
args[0][IFNAMSIZ - 1] = 0;
|
f_args[0][IFNAMSIZ - 1] = 0;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ UpdateDialog::~UpdateDialog() {
|
|||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: migrate to a net.cpp wrapper
|
||||||
void UpdateDialog::Download() {
|
void UpdateDialog::Download() {
|
||||||
const auto filename = QtCommon::Frontend::GetSaveFileName(
|
const auto filename = QtCommon::Frontend::GetSaveFileName(
|
||||||
tr("New Version Location"),
|
tr("New Version Location"),
|
||||||
@@ -161,7 +162,7 @@ void UpdateDialog::Download() {
|
|||||||
QString::number(response.status)));
|
QString::number(response.status)));
|
||||||
return;
|
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);
|
LOG_ERROR(Frontend, "GET to {}{} returned no content", m_asset.url, m_asset.path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user