From 8d717160e9e2c8c49767c00be71ff5f4a177a826 Mon Sep 17 00:00:00 2001 From: SSimco <37044560+SSimco@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:36:06 +0300 Subject: [PATCH] Update graphic packs --- .../ui/components/FilledSearchToolbar.kt | 4 + .../cemu/cemu/gamelist/GamesListScreen.kt | 10 +- .../cemu/graphicpacks/GraphicPackDataNode.kt | 30 -- .../graphicpacks/GraphicPackDataViewModel.kt | 76 ----- .../cemu/cemu/graphicpacks/GraphicPackNode.kt | 8 - ...PackSectionNode.kt => GraphicPackNodes.kt} | 50 +++- .../cemu/graphicpacks/GraphicPackViewModel.kt | 5 - .../graphicpacks/GraphicPacksDownloader.kt | 2 +- .../graphicpacks/GraphicPacksListViewModel.kt | 149 ---------- .../graphicpacks/GraphicPacksNavigation.kt | 86 +----- ...cPacksScreens.kt => GraphicPacksScreen.kt} | 205 +++++++------ .../graphicpacks/GraphicPacksViewModel.kt | 274 ++++++++++++++++++ .../cemu/nativeinterface/NativeAccount.kt | 8 +- .../cemu/titlemanager/TitleListViewModel.kt | 7 +- .../cemu/titlemanager/TitleManagerScreen.kt | 14 +- .../usecases/CompressTitleUseCase.kt | 2 + .../usecases/InstallTitleUseCase.kt | 3 +- 17 files changed, 456 insertions(+), 477 deletions(-) delete mode 100644 src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataNode.kt delete mode 100644 src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataViewModel.kt delete mode 100644 src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNode.kt rename src/android/app/src/main/java/info/cemu/cemu/graphicpacks/{GraphicPackSectionNode.kt => GraphicPackNodes.kt} (71%) delete mode 100644 src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackViewModel.kt delete mode 100644 src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksListViewModel.kt rename src/android/app/src/main/java/info/cemu/cemu/graphicpacks/{GraphicPacksScreens.kt => GraphicPacksScreen.kt} (74%) create mode 100644 src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksViewModel.kt diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/FilledSearchToolbar.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/FilledSearchToolbar.kt index 64fb4b82..b0329c80 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/FilledSearchToolbar.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/FilledSearchToolbar.kt @@ -1,5 +1,6 @@ package info.cemu.cemu.common.ui.components +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row @@ -53,6 +54,9 @@ fun FilledSearchToolbar( actions: @Composable RowScope.() -> Unit = {}, ) { var searchBarActive by remember { mutableStateOf(false) } + + BackHandler(enabled = searchBarActive) { searchBarActive = false } + TopAppBar( actions = actions, title = { diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt index 3c550b52..ba952710 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt @@ -4,7 +4,7 @@ package info.cemu.cemu.gamelist - +import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -92,6 +92,7 @@ fun GamesListScreen( gameListViewModel.setFilterText("") } } + Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { @@ -196,7 +197,7 @@ private fun GameList( } @Composable -fun ShaderCachesConfirmationDialog( +private fun ShaderCachesConfirmationDialog( gameName: String, onDismissRequest: () -> Unit, onConfirm: () -> Unit, @@ -227,7 +228,7 @@ fun ShaderCachesConfirmationDialog( } @Composable -fun GameListItem( +private fun GameListItem( onStartGame: (Game) -> Unit, onIsFavoriteChanged: (Boolean) -> Unit, onEditGameProfile: () -> Unit, @@ -290,7 +291,7 @@ fun GameListItem( } @Composable -fun GameContextMenu( +private fun GameContextMenu( expanded: Boolean, onDismissRequest: () -> Unit, onIsFavoriteChanged: (Boolean) -> Unit, @@ -354,4 +355,3 @@ fun GameContextMenu( ) } } - diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataNode.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataNode.kt deleted file mode 100644 index a1640ca6..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataNode.kt +++ /dev/null @@ -1,30 +0,0 @@ -package info.cemu.cemu.graphicpacks - -class GraphicPackDataNode( - val id: Long, - name: String, - val path: String, - enabled: Boolean, - val parent: GraphicPackSectionNode?, -) : - GraphicPackNode(name) { - constructor( - id: Long, - name: String, - path: String, - enabled: Boolean, - titleIdInstalled: Boolean, - parentNode: GraphicPackSectionNode, - ) : this(id, name, path, enabled, parentNode) { - this.titleIdInstalled = titleIdInstalled - } - - var enabled: Boolean = enabled - set(value) { - if (field == value) { - return - } - field = value - parent?.updateEnabledCount(value) - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataViewModel.kt deleted file mode 100644 index eb2869aa..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackDataViewModel.kt +++ /dev/null @@ -1,76 +0,0 @@ -package info.cemu.cemu.graphicpacks - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory -import info.cemu.cemu.nativeinterface.NativeGraphicPacks -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -data class Preset( - val index: Int, - val category: String?, - val activePreset: String, - val presets: List, -) - -class GraphicPackDataViewModel(private val graphicPackNode: GraphicPackDataNode) : ViewModel() { - private val nativeGraphicPack = NativeGraphicPacks.getGraphicPack(graphicPackNode.id) - val description = nativeGraphicPack?.description ?: "" - private var nativePresets: List = emptyList() - private val _presets = MutableStateFlow>(emptyList()) - val presets = _presets.asStateFlow() - - val name = graphicPackNode.name - - private var _enabled = MutableStateFlow(graphicPackNode.enabled) - val enabled = _enabled.asStateFlow() - fun setEnabled(enabled: Boolean) { - nativeGraphicPack?.setActive(enabled) - _enabled.value = enabled - graphicPackNode.enabled = enabled - } - - init { - refreshPresets() - } - - private fun refreshPresets() { - nativeGraphicPack?.reloadPresets() - if (nativePresets == nativeGraphicPack?.presets) { - return - } - nativePresets = nativeGraphicPack?.presets ?: emptyList() - nativePresets.mapIndexed { index, preset -> - Preset( - index = index, - category = preset.category, - activePreset = preset.activePreset, - presets = preset.presets, - ) - }.let { - _presets.value = it - } - } - - fun setActivePreset(index: Int, activePreset: String) { - _presets.value = _presets.value.toMutableList().apply { - set(index, get(index).copy(activePreset = activePreset)) - } - nativePresets[index].activePreset = activePreset - refreshPresets() - } - - companion object { - val GRAPHIC_PACK_KEY = object : CreationExtras.Key {} - val Factory: ViewModelProvider.Factory = viewModelFactory { - initializer { - GraphicPackDataViewModel( - this[GRAPHIC_PACK_KEY] as GraphicPackDataNode - ) - } - } - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNode.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNode.kt deleted file mode 100644 index 1e550acc..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNode.kt +++ /dev/null @@ -1,8 +0,0 @@ -package info.cemu.cemu.graphicpacks - -abstract class GraphicPackNode( - val name: String? -) { - var titleIdInstalled: Boolean = false - protected set -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionNode.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNodes.kt similarity index 71% rename from src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionNode.kt rename to src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNodes.kt index e7e85117..ac951e28 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackSectionNode.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackNodes.kt @@ -3,21 +3,27 @@ package info.cemu.cemu.graphicpacks import info.cemu.cemu.nativeinterface.NativeGraphicPacks import kotlin.math.max +sealed class GraphicPackNode( + val name: String?, + val parent: GraphicPackSectionNode? +) { + var titleIdInstalled: Boolean = false + protected set + + fun isRoot() = parent == null +} + class GraphicPackSectionNode : GraphicPackNode { - constructor() : super(null) { - this.parent = null - } + constructor() : super(null, null) constructor( name: String, titleIdInstalled: Boolean, - parent: GraphicPackSectionNode? - ) : super(name) { + parent: GraphicPackSectionNode + ) : super(name, parent) { this.titleIdInstalled = titleIdInstalled - this.parent = parent } - val parent: GraphicPackSectionNode? var enabledGraphicPacksCount: Int = 0 private set var children: ArrayList = ArrayList() @@ -79,4 +85,32 @@ class GraphicPackSectionNode : GraphicPackNode { enabledGraphicPacksCount = max(0, enabledGraphicPacksCount + if (enabled) 1 else -1) parent?.updateEnabledCount(enabled) } -} \ No newline at end of file +} + +class GraphicPackDataNode( + val id: Long, + name: String, + val path: String, + enabled: Boolean, + parent: GraphicPackSectionNode, +) : GraphicPackNode(name, parent) { + constructor( + id: Long, + name: String, + path: String, + enabled: Boolean, + titleIdInstalled: Boolean, + parentNode: GraphicPackSectionNode, + ) : this(id, name, path, enabled, parentNode) { + this.titleIdInstalled = titleIdInstalled + } + + var enabled: Boolean = enabled + set(value) { + if (field == value) { + return + } + field = value + parent?.updateEnabledCount(value) + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackViewModel.kt deleted file mode 100644 index c95ba767..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPackViewModel.kt +++ /dev/null @@ -1,5 +0,0 @@ -package info.cemu.cemu.graphicpacks - -import androidx.lifecycle.ViewModel - -class GraphicPackViewModel(var graphicPackNode: GraphicPackNode? = null) : ViewModel() \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksDownloader.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksDownloader.kt index 0677475d..b9a633ee 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksDownloader.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksDownloader.kt @@ -124,7 +124,7 @@ class GraphicPacksDownloader { graphicPacksTempDir.deleteRecursively() unzip( response.body.byteStream(), - graphicPacksTempDir.path.toString() + graphicPacksTempDir.path ) graphicPacksTempDir.resolve("version.txt").writeText(version) val downloadedGraphicPacksDir = diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksListViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksListViewModel.kt deleted file mode 100644 index 46d2d849..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksListViewModel.kt +++ /dev/null @@ -1,149 +0,0 @@ -package info.cemu.cemu.graphicpacks - -import android.content.Context -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import info.cemu.cemu.nativeinterface.NativeGameTitles -import info.cemu.cemu.nativeinterface.NativeGraphicPacks -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -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.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import java.util.regex.Pattern - -class GraphicPacksListViewModel : ViewModel() { - private val installedTitleIds = NativeGameTitles.getInstalledGamesTitleIds() - private var rootNode = GraphicPackSectionNode() - - private val _installedOnly = MutableStateFlow(installedTitleIds.size > 1) - val installedOnly = _installedOnly.asStateFlow() - fun setInstalledOnly(installedOnly: Boolean) { - _installedOnly.value = installedOnly - } - - private val _downloadStatus = MutableStateFlow(null) - private suspend fun updateDownloadStatus(status: GraphicPacksDownloadStatus?) { - _downloadStatus.first { it == null } - _downloadStatus.value = status - } - - val downloadStatus = _downloadStatus.asStateFlow() - - private var downloadJob: Job? = null - fun downloadNewUpdate(context: Context) { - if (_downloadStatus.value != null) return - downloadJob = viewModelScope.launch { - try { - GraphicPacksDownloader.download(context) { updateDownloadStatus(it) } - refreshGraphicPacks() - } catch (_: Exception) { - updateDownloadStatus(GraphicPacksDownloadStatus.ERROR) - } - } - } - - fun downloadStatusRead() { - _downloadStatus.value = null - } - - fun cancelDownload() { - val oldDownloadJob = downloadJob ?: return - downloadJob = null - viewModelScope.launch { - oldDownloadJob.cancelAndJoin() - updateDownloadStatus(GraphicPacksDownloadStatus.CANCELED) - } - } - - private val _filterText = MutableStateFlow("") - val filterText: StateFlow = _filterText - fun setFilterText(filterText: String) { - _filterText.value = filterText - } - - private val filterPattern = filterText.map { filterText -> - if (filterText.isBlank()) { - return@map null - } - return@map buildString { - filterText.trim() - .split(" ".toRegex()) - .forEach { append("(?=.*" + Pattern.quote(it) + ")") } - append(".*") - }.toPattern(Pattern.CASE_INSENSITIVE) - } - - private val _graphicPackDataNodes = MutableStateFlow>(emptyList()) - val graphicPackDataNodes: StateFlow> = - combine( - _graphicPackDataNodes, - installedOnly, - filterPattern - ) { graphicPackNodes, installedOnly, pattern -> - if (!installedOnly && pattern == null) { - return@combine graphicPackNodes - } - if (pattern != null) { - return@combine graphicPackNodes.filter { - pattern.matcher(it.path).matches() && (it.titleIdInstalled || !installedOnly) - } - } - return@combine graphicPackNodes.filter { it.titleIdInstalled } - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - emptyList() - ) - - private val _graphicPackNodes = MutableStateFlow>(emptyList()) - val graphicPackNodes: StateFlow> = - installedOnly.combine(_graphicPackNodes) { installedOnly, graphicPackNodes -> - if (installedOnly) { - return@combine graphicPackNodes.filter { it.titleIdInstalled } - } - return@combine graphicPackNodes - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - emptyList() - ) - - init { - refreshGraphicPacks() - } - - private fun MutableList.fillWithDataNodes(graphicPackSectionNode: GraphicPackSectionNode): MutableList { - for (node in graphicPackSectionNode.children) { - when (node) { - is GraphicPackSectionNode -> fillWithDataNodes(node) - is GraphicPackDataNode -> add(node) - } - } - return this - } - - private fun refreshGraphicPacks() { - rootNode = GraphicPackSectionNode().apply { - NativeGraphicPacks.getGraphicPackBasicInfos().forEach { - val hasTitleInstalled = it.titleIds.any { titleId -> titleId in installedTitleIds } - addGraphicPackDataByTokens(it, hasTitleInstalled) - } - sort() - } - _graphicPackDataNodes.value = - mutableListOf().fillWithDataNodes(rootNode) - _graphicPackNodes.value = rootNode.children.toList() - } - - companion object { - private val GraphicPacksDownloader = GraphicPacksDownloader() - } -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksNavigation.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksNavigation.kt index 87ca48e3..f5cdc078 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksNavigation.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksNavigation.kt @@ -1,93 +1,17 @@ package info.cemu.cemu.graphicpacks -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewmodel.MutableCreationExtras -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation import kotlinx.serialization.Serializable @Serializable object GraphicPacksRoute -private object GraphicPackRoutes { - @Serializable - object GraphicPacksRootSectionRoute - - @Serializable - object GraphicPackSectionScreenRoute - - @Serializable - object GraphicPackDataScreenRoute -} - -private inline fun NavGraphBuilder.composableNestedGraphicPacks( - navController: NavController, - noinline content: @Composable (AnimatedContentScope.(NavBackStackEntry, U) -> Unit), -) { - composable { backStackEntry -> - val previousBackStackEntry = - navController.previousBackStackEntry ?: return@composable - val graphicPackNode = - viewModel(previousBackStackEntry).graphicPackNode - if (graphicPackNode == null || graphicPackNode !is U) { - return@composable - } - content(backStackEntry, graphicPackNode) - } -} - -private fun graphicPacksNavigate(navController: NavController, graphicPackNode: GraphicPackNode) { - when (graphicPackNode) { - is GraphicPackSectionNode -> navController.navigate(GraphicPackRoutes.GraphicPackSectionScreenRoute) - is GraphicPackDataNode -> navController.navigate(GraphicPackRoutes.GraphicPackDataScreenRoute) - } -} - fun NavGraphBuilder.graphicPacksNavigation(navController: NavHostController) { - navigation(startDestination = GraphicPackRoutes.GraphicPacksRootSectionRoute) { - composable { backStackEntry -> - val graphicPackViewModel: GraphicPackViewModel = viewModel(backStackEntry) - GraphicPacksRootSectionScreen( - navigateBack = { navController.popBackStack() }, - graphicPackNodeNavigate = { - graphicPackViewModel.graphicPackNode = it - graphicPacksNavigate(navController, it) - } - ) - } - composableNestedGraphicPacks( - navController - ) { backStackEntry, graphicPackNode -> - val graphicPacksViewModel: GraphicPackViewModel = viewModel(backStackEntry) - GraphicPacksSectionScreen( - navigateBack = { navController.popBackStack() }, - graphicPackNodeNavigate = { - graphicPacksViewModel.graphicPackNode = it - graphicPacksNavigate(navController, it) - }, - graphicPackSectionNode = graphicPackNode, - ) - } - composableNestedGraphicPacks( - navController - ) { backStackEntry, graphicPackNode -> - val graphicPackDataViewModel: GraphicPackDataViewModel = viewModel( - viewModelStoreOwner = backStackEntry, - factory = GraphicPackDataViewModel.Factory, - extras = MutableCreationExtras().apply { - set(GraphicPackDataViewModel.GRAPHIC_PACK_KEY, graphicPackNode) - } - ) - GraphicPackDataScreen( - navigateBack = { navController.popBackStack() }, - graphicPackDataViewModel = graphicPackDataViewModel, - ) - } + composable { + GraphicPacksScreen( + navigateBack = { navController.popBackStack() } + ) } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreens.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt similarity index 74% rename from src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreens.kt rename to src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt index ea7d6227..cc6d15ae 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreens.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt @@ -2,6 +2,7 @@ package info.cemu.cemu.graphicpacks +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -62,28 +63,29 @@ import info.cemu.cemu.common.ui.components.SingleSelection import info.cemu.cemu.common.ui.localization.tr import kotlinx.coroutines.launch - @Composable -fun GraphicPacksRootSectionScreen( +fun GraphicPacksScreen( navigateBack: () -> Unit, - graphicPackNodeNavigate: (GraphicPackNode) -> Unit, - graphicPacksListViewModel: GraphicPacksListViewModel = viewModel(), + graphicPacksViewModel: GraphicPacksViewModel = viewModel(), ) { - val graphicPackNodes by graphicPacksListViewModel.graphicPackNodes.collectAsState() - val graphicPackDataNodes by graphicPacksListViewModel.graphicPackDataNodes.collectAsState() - val query by graphicPacksListViewModel.filterText.collectAsState() - val installedOnly by graphicPacksListViewModel.installedOnly.collectAsState() + val graphicPackDataNodes by graphicPacksViewModel.graphicPackDataNodes.collectAsState() + val query by graphicPacksViewModel.filterText.collectAsState() + val installedOnly by graphicPacksViewModel.installedOnly.collectAsState() var showGraphicPackSearch by rememberSaveable { mutableStateOf(false) } - val downloadStatus by graphicPacksListViewModel.downloadStatus.collectAsState() + val downloadStatus by graphicPacksViewModel.downloadStatus.collectAsState() val snackbarScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } var downloadDialogText by rememberSaveable { mutableStateOf(null) } val context = LocalContext.current + val currentNodeState = graphicPacksViewModel.currentNode.collectAsState() + val currentNode = currentNodeState.value + val graphicPackDataState = graphicPacksViewModel.currentDataGraphicPack.collectAsState() + val graphicPackData = graphicPackDataState.value downloadStatus?.let { status -> downloadDialogText = downloadStatusToDialogTextString(status) LaunchedEffect(status) { - graphicPacksListViewModel.downloadStatusRead() + graphicPacksViewModel.downloadStatusRead() val downloadNotificationText = downloadStatusToNotificationString(status) ?: return@LaunchedEffect @@ -94,26 +96,39 @@ fun GraphicPacksRootSectionScreen( } } } - fun onNavigateBack() { + + fun handleBack() { if (showGraphicPackSearch) { showGraphicPackSearch = false - } else { - navigateBack() + return } + + if (!currentNodeState.value.isRoot()) { + graphicPacksViewModel.navigateBack() + return + } + + navigateBack() + } + + BackHandler(enabled = showGraphicPackSearch || !currentNodeState.value.isRoot()) { + handleBack() } ScreenContentLazy( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, actions = { - GraphicPacksRootSectionActions( - showMainActions = !showGraphicPackSearch, - onSearchClicked = { - showGraphicPackSearch = true - }, - onDownloadClicked = { graphicPacksListViewModel.downloadNewUpdate(context) }, - installedOnlyChecked = installedOnly, - installedOnlyValueChange = graphicPacksListViewModel::setInstalledOnly, - ) + if (currentNode.isRoot()) { + GraphicPacksRootSectionActions( + showMainActions = !showGraphicPackSearch, + onSearchClicked = { + showGraphicPackSearch = true + }, + onDownloadClicked = { graphicPacksViewModel.downloadNewUpdate(context) }, + installedOnlyChecked = installedOnly, + installedOnlyValueChange = graphicPacksViewModel::setInstalledOnly, + ) + } }, appBarTitle = { Box( @@ -121,35 +136,47 @@ fun GraphicPacksRootSectionScreen( .height(IntrinsicSize.Min) .padding(8.dp), ) { - if (showGraphicPackSearch) { + if (showGraphicPackSearch && currentNode.isRoot()) { SearchToolbarInput( value = query, - onValueChange = graphicPacksListViewModel::setFilterText, + onValueChange = graphicPacksViewModel::setFilterText, hint = tr("Search graphic packs"), ) } else { - DefaultAppBarTitle(tr("Graphic packs")) + DefaultAppBarTitle(currentNode.name ?: tr("Graphic packs")) } } }, - navigateBack = ::onNavigateBack, + navigateBack = ::handleBack, ) { - if (showGraphicPackSearch) { + if (showGraphicPackSearch && currentNode.isRoot()) { graphicPackDataSearchItems( nodes = graphicPackDataNodes, - onClick = graphicPackNodeNavigate, + onClick = { graphicPacksViewModel.navigateTo(it) }, ) - } else { + return@ScreenContentLazy + } + + if (currentNode is GraphicPackSectionNode) { graphicPackSectionItems( - nodes = graphicPackNodes, - onClick = graphicPackNodeNavigate, + installedOnly = installedOnly, + nodes = currentNode.children, + onClick = { graphicPacksViewModel.navigateTo(it) }, ) } + + if (graphicPackData != null) { + graphicPackDataNodeItem( + graphicPacksViewModel = graphicPacksViewModel, + graphicPackData = graphicPackData + ) + } + } if (downloadDialogText != null) { GraphicPacksDownloadDialog( onCancelRequest = { - graphicPacksListViewModel.cancelDownload() + graphicPacksViewModel.cancelDownload() }, text = downloadDialogText!!, ) @@ -173,7 +200,7 @@ private fun downloadStatusToNotificationString(downloadStatus: GraphicPacksDownl } @Composable -fun GraphicPacksDownloadDialog( +private fun GraphicPacksDownloadDialog( onCancelRequest: () -> Unit, text: String, ) { @@ -206,7 +233,7 @@ fun GraphicPacksDownloadDialog( } @Composable -fun GraphicPacksRootSectionActions( +private fun GraphicPacksRootSectionActions( showMainActions: Boolean, onSearchClicked: () -> Unit, onDownloadClicked: () -> Unit, @@ -261,7 +288,7 @@ fun GraphicPacksRootSectionActions( } } -fun LazyListScope.graphicPackDataSearchItems( +private fun LazyListScope.graphicPackDataSearchItems( nodes: List, onClick: (GraphicPackDataNode) -> Unit, ) { @@ -291,80 +318,52 @@ fun LazyListScope.graphicPackDataSearchItems( } } -@Composable -fun GraphicPacksSectionScreen( - navigateBack: () -> Unit, - graphicPackNodeNavigate: (GraphicPackNode) -> Unit, - graphicPackSectionNode: GraphicPackSectionNode, +private fun LazyListScope.graphicPackDataNodeItem( + graphicPacksViewModel: GraphicPacksViewModel, + graphicPackData: GraphicPackData ) { - val appBarText = graphicPackSectionNode.name ?: tr("Graphic packs") - ScreenContentLazy( - appBarText = appBarText, - navigateBack = navigateBack, - ) { - graphicPackSectionItems( - nodes = graphicPackSectionNode.children, - onClick = graphicPackNodeNavigate, + item { + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = tr("Enabled")) + Switch( + modifier = Modifier.padding(horizontal = 8.dp), + checked = graphicPackData.active, + onCheckedChange = graphicPacksViewModel::setCurrentGraphicPackActive, + ) + } + } + item { + Text( + modifier = Modifier.padding(8.dp), + text = graphicPackData.description + ) + } + items(items = graphicPackData.presets) { + SingleSelection( + modifier = Modifier.animateItem(), + label = it.category ?: tr("Active preset"), + choices = it.choices, + choice = it.activeChoice, + onChoiceChanged = { activePreset -> + graphicPacksViewModel.setCurrentGraphicPackActivePreset( + it.index, + activePreset + ) + } ) } } -@Composable -fun GraphicPackDataScreen( - navigateBack: () -> Unit, - graphicPackDataViewModel: GraphicPackDataViewModel, -) { - val appBarText = graphicPackDataViewModel.name ?: tr("Graphic packs") - val enabled by graphicPackDataViewModel.enabled.collectAsState() - val presets by graphicPackDataViewModel.presets.collectAsState() - - ScreenContentLazy( - appBarText = appBarText, - navigateBack = navigateBack, - ) { - item { - Row( - modifier = Modifier.padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = tr("Enabled")) - Switch( - modifier = Modifier.padding(horizontal = 8.dp), - checked = enabled, - onCheckedChange = graphicPackDataViewModel::setEnabled, - ) - } - } - item { - Text( - modifier = Modifier.padding(8.dp), - text = graphicPackDataViewModel.description - ) - } - items(items = presets) { - SingleSelection( - modifier = Modifier.animateItem(), - label = it.category ?: tr("Active preset"), - choices = it.presets, - choice = it.activePreset, - onChoiceChanged = { activePreset -> - graphicPackDataViewModel.setActivePreset( - it.index, - activePreset - ) - } - ) - } - } -} - - private fun LazyListScope.graphicPackSectionItems( nodes: List, + installedOnly: Boolean, onClick: (GraphicPackNode) -> Unit, ) { items( - items = nodes, + items = if (installedOnly) nodes.filter { it.titleIdInstalled } else nodes, ) { GraphicPackListItem( label = it.name, @@ -380,7 +379,7 @@ private fun LazyListScope.graphicPackSectionItems( } @Composable -fun GraphicPackDataListItemIcon(isEnabled: Boolean) { +private fun GraphicPackDataListItemIcon(isEnabled: Boolean) { GraphicPackListItemIcon( painter = painterResource(R.drawable.ic_package_2), showExtraInfo = isEnabled, @@ -397,7 +396,7 @@ fun GraphicPackDataListItemIcon(isEnabled: Boolean) { } @Composable -fun GraphicPackSectionListItemIcon(numberOfEnabledPacks: Int) { +private fun GraphicPackSectionListItemIcon(numberOfEnabledPacks: Int) { GraphicPackListItemIcon( painter = painterResource(R.drawable.ic_lists), showExtraInfo = numberOfEnabledPacks > 0, @@ -414,7 +413,7 @@ fun GraphicPackSectionListItemIcon(numberOfEnabledPacks: Int) { } @Composable -fun GraphicPackListItemIcon( +private fun GraphicPackListItemIcon( painter: Painter, showExtraInfo: Boolean, extraInfoContent: @Composable () -> Unit, @@ -434,7 +433,7 @@ fun GraphicPackListItemIcon( } @Composable -fun GraphicPackListItem( +private fun GraphicPackListItem( label: String?, onClick: () -> Unit, modifier: Modifier, diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksViewModel.kt new file mode 100644 index 00000000..4813a44c --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksViewModel.kt @@ -0,0 +1,274 @@ +package info.cemu.cemu.graphicpacks + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import info.cemu.cemu.nativeinterface.NativeGameTitles +import info.cemu.cemu.nativeinterface.NativeGraphicPacks +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +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.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.util.regex.Pattern + +data class Preset( + val index: Int, + val category: String?, + val activeChoice: String, + val choices: List, +) { + companion object { + fun fromNativeGraphicPack(graphicPack: NativeGraphicPacks.GraphicPack): List { + return graphicPack.presets.mapIndexed { index, preset -> + Preset( + index = index, + category = preset.category, + activeChoice = preset.activePreset, + choices = preset.presets, + ) + } + } + + } +} + +data class GraphicPackData( + val description: String, + val active: Boolean, + val presets: List, +) + +private fun MutableList.fillWithDataNodes(graphicPackSectionNode: GraphicPackSectionNode): MutableList { + for (node in graphicPackSectionNode.children) { + when (node) { + is GraphicPackSectionNode -> fillWithDataNodes(node) + is GraphicPackDataNode -> add(node) + } + } + return this +} + +class GraphicPacksViewModel : ViewModel() { + private var rootNode = GraphicPackSectionNode() + + val installedTitleIds = NativeGameTitles.getInstalledGamesTitleIds() + + private val _installedOnly = MutableStateFlow(installedTitleIds.size > 1) + val installedOnly = _installedOnly.asStateFlow() + fun setInstalledOnly(installedOnly: Boolean) { + _installedOnly.value = installedOnly + } + + private val path = MutableStateFlow(listOf(rootNode)) + + val currentNode = path.map { it.last() } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + rootNode + ) + + private var currentNativeGraphicPack: NativeGraphicPacks.GraphicPack? = null + private val _currentDataGraphicPack = MutableStateFlow(null) + val currentDataGraphicPack = _currentDataGraphicPack.asStateFlow() + + private fun setCurrentDataGraphicPack(graphicPackDataNode: GraphicPackDataNode) { + val nativeGraphicPack = NativeGraphicPacks.getGraphicPack(graphicPackDataNode.id) ?: return + currentNativeGraphicPack = nativeGraphicPack + _currentDataGraphicPack.value = GraphicPackData( + description = nativeGraphicPack.description, + active = nativeGraphicPack.isActive(), + presets = Preset.fromNativeGraphicPack(nativeGraphicPack) + ) + } + + private fun clearCurrentDataGraphicPack() { + currentNativeGraphicPack = null + _currentDataGraphicPack.value = null + } + + fun setCurrentGraphicPackActive(active: Boolean) { + val dataNode = currentNode.value + if (dataNode !is GraphicPackDataNode) { + return + } + dataNode.enabled = active + currentNativeGraphicPack?.setActive(active) + _currentDataGraphicPack.value = _currentDataGraphicPack.value?.copy(active = active) + } + + private fun refreshCurrentGraphicPackPresets() { + val nativeGraphicPack = currentNativeGraphicPack ?: return + + val oldPresets = nativeGraphicPack.presets + + nativeGraphicPack.reloadPresets() + + if (oldPresets == nativeGraphicPack.presets) { + return + } + + _currentDataGraphicPack.value = _currentDataGraphicPack.value?.copy( + presets = Preset.fromNativeGraphicPack(nativeGraphicPack) + ) + } + + fun setCurrentGraphicPackActivePreset(index: Int, activePreset: String) { + val nativeGraphicPack = currentNativeGraphicPack ?: return + + var presets = _currentDataGraphicPack.value?.presets ?: return + + presets = presets.toMutableList().apply { + set(index, get(index).copy(activeChoice = activePreset)) + } + + _currentDataGraphicPack.value = _currentDataGraphicPack.value?.copy(presets = presets) + + nativeGraphicPack.presets[index].activePreset = activePreset + + refreshCurrentGraphicPackPresets() + } + + + fun navigateBack() { + val currentPath = path.value + val lastNode = currentPath.last() + + if (currentPath.size > 1) { + path.value = currentPath.dropLast(1) + } + + if (lastNode is GraphicPackDataNode) { + clearCurrentDataGraphicPack() + } + } + + fun navigateTo(node: GraphicPackNode) { + val currentPath = path.value + + if (node is GraphicPackSectionNode) { + path.value += node + return + } + + if (node !is GraphicPackDataNode) { + return + } + + setCurrentDataGraphicPack(node) + + if (currentPath.last() === node.parent) { + path.value += node + return + } + + val newPath = mutableListOf(node) + var currentNode = node.parent + while (currentNode != null) { + newPath.add(currentNode) + currentNode = currentNode.parent + } + + path.value = newPath.reversed() + } + + private val _downloadStatus = MutableStateFlow(null) + private suspend fun updateDownloadStatus(status: GraphicPacksDownloadStatus?) { + _downloadStatus.first { it == null } + _downloadStatus.value = status + } + + val downloadStatus = _downloadStatus.asStateFlow() + + private var downloadJob: Job? = null + fun downloadNewUpdate(context: Context) { + if (_downloadStatus.value != null) return + downloadJob = viewModelScope.launch { + try { + GraphicPacksDownloader.download(context) { updateDownloadStatus(it) } + refreshGraphicPacks() + } catch (_: Exception) { + updateDownloadStatus(GraphicPacksDownloadStatus.ERROR) + } + } + } + + fun downloadStatusRead() { + _downloadStatus.value = null + } + + fun cancelDownload() { + val oldDownloadJob = downloadJob ?: return + downloadJob = null + viewModelScope.launch { + oldDownloadJob.cancelAndJoin() + updateDownloadStatus(GraphicPacksDownloadStatus.CANCELED) + } + } + + private val _filterText = MutableStateFlow("") + val filterText: StateFlow = _filterText + fun setFilterText(filterText: String) { + _filterText.value = filterText + } + + private val filterPattern = filterText.map { filterText -> + if (filterText.isBlank()) { + return@map null + } + return@map buildString { + filterText.trim().split(" ".toRegex()) + .forEach { append("(?=.*" + Pattern.quote(it) + ")") } + append(".*") + }.toPattern(Pattern.CASE_INSENSITIVE) + } + + private val _graphicPackDataNodes = MutableStateFlow>(emptyList()) + val graphicPackDataNodes: StateFlow> = combine( + _graphicPackDataNodes, installedOnly, filterPattern + ) { graphicPackNodes, installedOnly, pattern -> + if (!installedOnly && pattern == null) { + return@combine graphicPackNodes + } + if (pattern != null) { + return@combine graphicPackNodes.filter { + pattern.matcher(it.path).matches() && (it.titleIdInstalled || !installedOnly) + } + } + return@combine graphicPackNodes.filter { it.titleIdInstalled } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptyList() + ) + + private fun refreshGraphicPacks() { + rootNode = GraphicPackSectionNode().apply { + NativeGraphicPacks.getGraphicPackBasicInfos().forEach { + val hasTitleInstalled = it.titleIds.any { titleId -> titleId in installedTitleIds } + addGraphicPackDataByTokens(it, hasTitleInstalled) + } + sort() + } + + path.value = listOf(rootNode) + + _graphicPackDataNodes.value = + mutableListOf().fillWithDataNodes(rootNode) + } + + init { + refreshGraphicPacks() + } + + companion object { + private val GraphicPacksDownloader = GraphicPacksDownloader() + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeAccount.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeAccount.kt index 1635ad1f..2edd48e1 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeAccount.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeAccount.kt @@ -5,7 +5,7 @@ import androidx.annotation.Keep object NativeAccount { const val MAX_ACCOUNT_COUNT = 12 const val MIN_ACCOUNT_COUNT = 1 - const val MIN_PERSISTENT_ID: UInt = 2147483649u + const val MIN_PERSISTENT_ID: UInt = 0x80000001u const val DEFAULT_MII_NAME = "default" object AccountGender { @@ -54,16 +54,22 @@ object NativeAccount { @Keep sealed interface OnlineValidationError + @Keep class MissingOTP : OnlineValidationError + @Keep class CorruptedOTP : OnlineValidationError + @Keep class MissingSEEPROM : OnlineValidationError + @Keep class CorruptedSEEPROM : OnlineValidationError + @Keep data class MissingFile(val file: String) : OnlineValidationError + @Keep data class AccountError(val accountError: Int) : OnlineValidationError diff --git a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleListViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleListViewModel.kt index b85eab94..f1b4e085 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleListViewModel.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleListViewModel.kt @@ -201,13 +201,13 @@ class TitleListViewModel : ViewModel() { val titleInstallProgress = installUseCase.progress fun installQueuedTitle(context: Context, callback: (InstallResult) -> Unit) { - val queued = _queuedTitleToInstall.value ?: return + val (titleUri, titleExistsStatus) = _queuedTitleToInstall.value ?: return _queuedTitleToInstall.value = null installUseCase.install( context = context, - titleUri = queued.first, - targetLocation = queued.second.targetLocation, + titleUri = titleUri, + targetLocation = titleExistsStatus.targetLocation, callback = callback, ) } @@ -246,6 +246,7 @@ class TitleListViewModel : ViewModel() { uri: Uri, onResult: (CompressResult) -> Unit ) { + _queuedTitleToCompress.value = null compressUseCase.compress(context, uri, onResult) } diff --git a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleManagerScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleManagerScreen.kt index efd3f9bb..27c1e1d2 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleManagerScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleManagerScreen.kt @@ -51,6 +51,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -107,6 +108,7 @@ fun TitleManagerScreen( when (it) { InstallResult.ERROR -> showNotificationMessage(tr("Error installing")) InstallResult.FINISHED -> showNotificationMessage(tr("Finished installing")) + InstallResult.NOT_ENOUGH_SPACE -> showNotificationMessage(tr("Not enough space")) } }, ) @@ -240,7 +242,7 @@ fun TitleManagerScreen( @Composable private fun TitleCompressProgressDialog(bytesWritten: Long?, onCancel: () -> Unit) { - var showCancelConfirmDialog by remember { mutableStateOf(false) } + var showCancelConfirmDialog by rememberSaveable { mutableStateOf(false) } AlertDialog( title = { Text(tr("Compressing title")) }, @@ -345,7 +347,7 @@ private fun TitleInstallProgressDialog( progress: Pair?, onCancel: () -> Unit, ) { - var showCancelConfirmDialog by remember { mutableStateOf(false) } + var showCancelConfirmDialog by rememberSaveable { mutableStateOf(false) } AlertDialog( title = { Text(tr("Installing title")) }, @@ -504,7 +506,7 @@ private fun > FilterRow( valueToLabel: @Composable (T) -> String, onToggle: (T) -> Unit ) { - var showOptions by remember { mutableStateOf(false) } + var showOptions by rememberSaveable { mutableStateOf(false) } Column( modifier = Modifier .padding(8.dp) @@ -561,7 +563,7 @@ private fun TitleEntryListItem( onDeleteRequest: () -> Unit, onCompressRequested: () -> Unit, ) { - var showDeleteConfirmationDialog by remember { mutableStateOf(false) } + var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } Card( colors = CardDefaults.cardColors( @@ -572,7 +574,7 @@ private fun TitleEntryListItem( .animateContentSize() .padding(8.dp), ) { - var showTitleInfo by remember { mutableStateOf(false) } + var showTitleInfo by rememberSaveable { mutableStateOf(false) } Row( modifier = Modifier.padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically @@ -622,7 +624,7 @@ private fun TitleDropDownMenu( onDeleteClicked: () -> Unit, onCompressClicked: () -> Unit, ) { - var expandMenu by remember { mutableStateOf(false) } + var expandMenu by rememberSaveable { mutableStateOf(false) } @Composable fun DropdownMenuItem(text: String, onClick: () -> Unit) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/CompressTitleUseCase.kt b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/CompressTitleUseCase.kt index 3cb2824b..e40c2a7c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/CompressTitleUseCase.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/CompressTitleUseCase.kt @@ -29,6 +29,8 @@ class CompressTitleUseCase(private val scope: CoroutineScope) { scope.launch(Dispatchers.IO) { progressJob?.cancelAndJoin() NativeGameTitles.cancelTitleCompression() + _inProgress.value = false + _progress.value = null } } diff --git a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/InstallTitleUseCase.kt b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/InstallTitleUseCase.kt index c4d0e52d..fbe402f5 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/InstallTitleUseCase.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/InstallTitleUseCase.kt @@ -29,6 +29,7 @@ import kotlin.random.nextUInt enum class InstallResult{ ERROR, FINISHED, + NOT_ENOUGH_SPACE, } private sealed class DirEntry { @@ -87,7 +88,7 @@ class InstallTitleUseCase( ) if (totalSize > mlcPath.toFile().freeSpace) { - callback(InstallResult.ERROR) + callback(InstallResult.NOT_ENOUGH_SPACE) return@launch }