From 91ce31ca9f12e9f9f27ecdc7259aaaa909c22e14 Mon Sep 17 00:00:00 2001 From: SSimco <37044560+SSimco@users.noreply.github.com> Date: Sat, 23 Aug 2025 12:31:12 +0300 Subject: [PATCH] Update title manager --- .../java/info/cemu/cemu/CemuApplication.kt | 6 +- .../main/java/info/cemu/cemu/MainActivity.kt | 6 +- .../info/cemu/cemu/about/AboutCemuScreen.kt | 4 +- .../ContentResolverExtensions.kt | 56 +++ .../context/ContextExtensions.kt | 2 +- .../inputdevice/InputDeviceExtensions.kt | 2 +- .../motionevent/MotionEventExtensions.kt | 2 +- .../collections/CollectionExtensions.kt | 6 + .../nativeenummapper/GameTitlesEnums.kt | 16 - .../common/nativeenummapper/InputEnums.kt | 13 - .../cemu/common/settings/SettingsManager.kt | 2 +- .../cemu/common/string/StringExtensions.kt | 3 +- .../{ => ui}/components/ActivityContent.kt | 4 +- .../cemu/common/{ => ui}/components/Button.kt | 2 +- .../components/ComposeExtensions.kt} | 2 +- .../{ => ui}/components/DrawerLayout.kt | 2 +- .../components/FilledSearchToolbar.kt | 2 +- .../cemu/common/{ => ui}/components/Header.kt | 2 +- .../{ => ui}/components/ScreenContent.kt | 2 +- .../{ => ui}/components/SingleSelection.kt | 4 +- .../cemu/common/{ => ui}/components/Slider.kt | 2 +- .../cemu/common/{ => ui}/components/Toggle.kt | 2 +- .../localization/NativeTranslationHelpers.kt | 25 ++ .../localization}/PoFile.kt | 2 +- .../localization}/Translation.kt | 5 +- .../cemu/cemu/common/{ => ui}/theme/Theme.kt | 2 +- .../cemu/cemu/emulation/EmulationActivity.kt | 2 +- .../emulation/EmulationTextInputEditText.kt | 2 +- .../info/cemu/cemu/emulation/InputHandler.kt | 4 +- .../inputoverlay}/CanvasExtensions.kt | 2 +- .../inputoverlay/inputs/DPadInput.kt | 4 +- .../emulation/inputoverlay/inputs/Joystick.kt | 2 +- .../inputoverlay/inputs/RectangleButton.kt | 2 +- .../inputoverlay/inputs/RoundButton.kt | 2 +- .../cemu/cemu/gamelist/GameDetailsScreen.kt | 6 +- .../java/info/cemu/cemu/gamelist/GameIcon.kt | 1 - .../cemu/gamelist/GameProfileEditScreen.kt | 10 +- .../cemu/cemu/gamelist/GamesListScreen.kt | 4 +- .../graphicpacks/GraphicPackSectionNode.kt | 16 +- .../graphicpacks/GraphicPacksListViewModel.kt | 7 +- .../cemu/graphicpacks/GraphicPacksScreens.kt | 10 +- .../cemu/nativeinterface/NativeGameTitles.kt | 11 + .../cemu/cemu/provider/DocumentsProvider.kt | 2 +- .../cemu/cemu/settings/SettingsHomeScreen.kt | 6 +- .../cemu/settings/account/AccountsScreen.kt | 10 +- .../cemu/settings/audio/AudoSettingsScreen.kt | 10 +- .../customdrivers/CustomDriversScreen.kt | 4 +- .../settings/gamespath/GamePathsScreen.kt | 4 +- .../settings/general/GeneralSettingsScreen.kt | 10 +- .../general/GeneralSettingsViewModel.kt | 4 +- .../graphics/GraphicsSettingsScreen.kt | 10 +- .../settings/input/ClassicControllerInputs.kt | 2 +- .../input/ControllerInputSettingsScreen.kt | 8 +- .../cemu/settings/input/InputItemGroup.kt | 2 +- .../cemu/cemu/settings/input/InputMapper.kt | 23 +- .../settings/input/InputSettingsScreen.kt | 11 +- .../settings/input/ProControllerInputs.kt | 2 +- .../cemu/cemu/settings/input/VPADInputs.kt | 4 +- .../settings/input/WiimoteControllerInputs.kt | 2 +- .../InputOverlaySettingsScreen.kt | 10 +- .../settings/overlay/OverlaySettingsScreen.kt | 12 +- .../cemu/cemu/titlemanager/TitleInstaller.kt | 2 - .../cemu/titlemanager/TitleListViewModel.kt | 396 +++--------------- .../cemu/titlemanager/TitleManagerScreen.kt | 72 ++-- .../usecases/CompressTitleUseCase.kt | 85 ++++ .../usecases/DeleteTitleUseCase.kt | 50 +++ .../usecases/InstallTitleUseCase.kt | 199 +++++++++ .../main/res/layout/activity_emulation.xml | 4 +- .../info/cemu/cemu/tests/ArchitectureTests.kt | 15 +- src/android/gradle/libs.versions.toml | 2 +- 70 files changed, 667 insertions(+), 555 deletions(-) create mode 100644 src/android/app/src/main/java/info/cemu/cemu/common/android/contentresolver/ContentResolverExtensions.kt rename src/android/app/src/main/java/info/cemu/cemu/common/{ => android}/context/ContextExtensions.kt (83%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => android}/inputdevice/InputDeviceExtensions.kt (88%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => android}/motionevent/MotionEventExtensions.kt (85%) create mode 100644 src/android/app/src/main/java/info/cemu/cemu/common/collections/CollectionExtensions.kt delete mode 100644 src/android/app/src/main/java/info/cemu/cemu/common/nativeenummapper/GameTitlesEnums.kt delete mode 100644 src/android/app/src/main/java/info/cemu/cemu/common/nativeenummapper/InputEnums.kt rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/ActivityContent.kt (83%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/Button.kt (97%) rename src/android/app/src/main/java/info/cemu/cemu/common/{format/Formatters.kt => ui/components/ComposeExtensions.kt} (88%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/DrawerLayout.kt (96%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/FilledSearchToolbar.kt (99%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/Header.kt (92%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/ScreenContent.kt (99%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/SingleSelection.kt (98%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/Slider.kt (97%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/components/Toggle.kt (97%) create mode 100644 src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/NativeTranslationHelpers.kt rename src/android/app/src/main/java/info/cemu/cemu/common/{translation => ui/localization}/PoFile.kt (97%) rename src/android/app/src/main/java/info/cemu/cemu/common/{translation => ui/localization}/Translation.kt (96%) rename src/android/app/src/main/java/info/cemu/cemu/common/{ => ui}/theme/Theme.kt (96%) rename src/android/app/src/main/java/info/cemu/cemu/{common/canvas => emulation/inputoverlay}/CanvasExtensions.kt (96%) delete mode 100644 src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleInstaller.kt create mode 100644 src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/CompressTitleUseCase.kt create mode 100644 src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/DeleteTitleUseCase.kt create mode 100644 src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/InstallTitleUseCase.kt diff --git a/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt b/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt index 71369a1c..651de7de 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt @@ -1,9 +1,9 @@ package info.cemu.cemu import android.app.Application -import info.cemu.cemu.common.context.internalFolder -import info.cemu.cemu.common.translation.setLanguage -import info.cemu.cemu.common.translation.setTranslations +import info.cemu.cemu.common.android.context.internalFolder +import info.cemu.cemu.common.ui.localization.setLanguage +import info.cemu.cemu.common.ui.localization.setTranslations import info.cemu.cemu.nativeinterface.NativeActiveSettings.initializeActiveSettings import info.cemu.cemu.nativeinterface.NativeActiveSettings.setInternalDir import info.cemu.cemu.nativeinterface.NativeActiveSettings.setNativeLibDir diff --git a/src/android/app/src/main/java/info/cemu/cemu/MainActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/MainActivity.kt index 263637f5..4f204812 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/MainActivity.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/MainActivity.kt @@ -34,9 +34,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import info.cemu.cemu.about.AboutCemuRoute import info.cemu.cemu.about.aboutCemuNavigation -import info.cemu.cemu.common.components.ActivityContent -import info.cemu.cemu.common.translation.TranslatableContent -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ActivityContent +import info.cemu.cemu.common.ui.localization.TranslatableContent +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.emulation.EmulationActivity import info.cemu.cemu.gamelist.GameListRoute import info.cemu.cemu.gamelist.gameListNavigation diff --git a/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt index 8b5eb7f0..37a42d7f 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt @@ -39,8 +39,8 @@ import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors import com.mikepenz.aboutlibraries.ui.compose.util.author import info.cemu.cemu.BuildConfig import info.cemu.cemu.R -import info.cemu.cemu.common.components.ScreenContentLazy -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ScreenContentLazy +import info.cemu.cemu.common.ui.localization.tr import kotlinx.collections.immutable.persistentListOf typealias AboutLibrary = com.mikepenz.aboutlibraries.entity.Library diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/android/contentresolver/ContentResolverExtensions.kt b/src/android/app/src/main/java/info/cemu/cemu/common/android/contentresolver/ContentResolverExtensions.kt new file mode 100644 index 00000000..edacfe6b --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/common/android/contentresolver/ContentResolverExtensions.kt @@ -0,0 +1,56 @@ +package info.cemu.cemu.common.android.contentresolver + +import android.content.ContentResolver +import android.net.Uri +import android.provider.DocumentsContract +import kotlinx.coroutines.yield + +sealed class DocumentEntry { + data class File(val uri: Uri, val size: Long) : DocumentEntry() + data class Directory(val uri: Uri) : DocumentEntry() +} + +suspend fun ContentResolver.walkDocumentTree( + dirUri: Uri, + onEntry: (DocumentEntry) -> Unit, +) { + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( + dirUri, + DocumentsContract.getDocumentId(dirUri) + ) + + val cursor = query( + childrenUri, + arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_MIME_TYPE, + ), + null, + null, + null + ) + + cursor?.use { + while (it.moveToNext()) { + yield() + + val documentId = it.getString(0) + val documentUri = DocumentsContract.buildDocumentUriUsingTree(dirUri, documentId) + + val mimeType = it.getString(2) + if (mimeType != DocumentsContract.Document.MIME_TYPE_DIR) { + val sizeInBytes = it.getLong(1) + onEntry(DocumentEntry.File(documentUri, sizeInBytes)) + continue + } + + onEntry(DocumentEntry.Directory(documentUri)) + + walkDocumentTree( + DocumentsContract.buildDocumentUriUsingTree(dirUri, documentId), + onEntry, + ) + } + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/context/ContextExtensions.kt b/src/android/app/src/main/java/info/cemu/cemu/common/android/context/ContextExtensions.kt similarity index 83% rename from src/android/app/src/main/java/info/cemu/cemu/common/context/ContextExtensions.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/android/context/ContextExtensions.kt index 21d95bc0..855b92e0 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/context/ContextExtensions.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/android/context/ContextExtensions.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.context +package info.cemu.cemu.common.android.context import android.content.Context import java.io.File diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/inputdevice/InputDeviceExtensions.kt b/src/android/app/src/main/java/info/cemu/cemu/common/android/inputdevice/InputDeviceExtensions.kt similarity index 88% rename from src/android/app/src/main/java/info/cemu/cemu/common/inputdevice/InputDeviceExtensions.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/android/inputdevice/InputDeviceExtensions.kt index b8968649..a7faf6c6 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/inputdevice/InputDeviceExtensions.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/android/inputdevice/InputDeviceExtensions.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.inputdevice +package info.cemu.cemu.common.android.inputdevice import android.view.InputDevice diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/motionevent/MotionEventExtensions.kt b/src/android/app/src/main/java/info/cemu/cemu/common/android/motionevent/MotionEventExtensions.kt similarity index 85% rename from src/android/app/src/main/java/info/cemu/cemu/common/motionevent/MotionEventExtensions.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/android/motionevent/MotionEventExtensions.kt index 1fc9417f..df2abbea 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/motionevent/MotionEventExtensions.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/android/motionevent/MotionEventExtensions.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.motionevent +package info.cemu.cemu.common.android.motionevent import android.view.InputDevice import android.view.MotionEvent diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/collections/CollectionExtensions.kt b/src/android/app/src/main/java/info/cemu/cemu/common/collections/CollectionExtensions.kt new file mode 100644 index 00000000..0172ae17 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/common/collections/CollectionExtensions.kt @@ -0,0 +1,6 @@ +package info.cemu.cemu.common.collections + + +fun Set.toggleInSet(item: T): Set { + return if (item in this) this - item else this + item +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/nativeenummapper/GameTitlesEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/common/nativeenummapper/GameTitlesEnums.kt deleted file mode 100644 index b3b5626f..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/common/nativeenummapper/GameTitlesEnums.kt +++ /dev/null @@ -1,16 +0,0 @@ -package info.cemu.cemu.common.nativeenummapper - -import info.cemu.cemu.common.translation.tr -import info.cemu.cemu.nativeinterface.NativeGameTitles.ConsoleRegion - -fun regionToString(region: Int): String = when (region) { - ConsoleRegion.JPN -> tr("Japan") - ConsoleRegion.USA -> tr("USA") - ConsoleRegion.EUR -> tr("Europe") - ConsoleRegion.AUS_DEPR -> tr("Australia") - ConsoleRegion.CHN -> tr("China") - ConsoleRegion.KOR -> tr("Korea") - ConsoleRegion.TWN -> tr("Taiwan") - ConsoleRegion.AUTO -> tr("Auto") - else -> tr("Many") -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/nativeenummapper/InputEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/common/nativeenummapper/InputEnums.kt deleted file mode 100644 index 4ac2ad80..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/common/nativeenummapper/InputEnums.kt +++ /dev/null @@ -1,13 +0,0 @@ -package info.cemu.cemu.common.nativeenummapper - -import info.cemu.cemu.common.translation.tr -import info.cemu.cemu.nativeinterface.NativeInput.EmulatedControllerType - -fun controllerTypeToString(type: Int) = when (type) { - EmulatedControllerType.DISABLED -> tr("Disabled") - EmulatedControllerType.VPAD -> tr("Wii U GamePad") - EmulatedControllerType.PRO -> tr("Wii U Pro Controller") - EmulatedControllerType.WIIMOTE -> tr("Wiimote") - EmulatedControllerType.CLASSIC -> tr("Wii U Classic Controller") - else -> throw IllegalArgumentException("Invalid controller type: $type") -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/settings/SettingsManager.kt b/src/android/app/src/main/java/info/cemu/cemu/common/settings/SettingsManager.kt index 241e1a7d..e0b16570 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/settings/SettingsManager.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/settings/SettingsManager.kt @@ -2,7 +2,7 @@ package info.cemu.cemu.common.settings import android.content.Context import android.content.SharedPreferences -import info.cemu.cemu.common.translation.DEFAULT_LANGUAGE +import info.cemu.cemu.common.ui.localization.DEFAULT_LANGUAGE import kotlin.getValue class EmulationScreenSettings(sharedPreferences: SharedPreferences) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/string/StringExtensions.kt b/src/android/app/src/main/java/info/cemu/cemu/common/string/StringExtensions.kt index 9bf41710..78facc1a 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/string/StringExtensions.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/string/StringExtensions.kt @@ -8,11 +8,10 @@ fun String.toIntOrZero() = toIntOrNull() ?: 0 fun String.isContentUri() = startsWith("content://") - fun String.parseHexOrNull(): UInt? { return try { toUInt(16) } catch (_: NumberFormatException) { null } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/ActivityContent.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ActivityContent.kt similarity index 83% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/ActivityContent.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ActivityContent.kt index 76902bdf..9ade7d83 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/ActivityContent.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ActivityContent.kt @@ -1,11 +1,11 @@ -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import info.cemu.cemu.common.theme.CemuTheme +import info.cemu.cemu.common.ui.theme.CemuTheme @Composable fun ActivityContent(content: @Composable () -> Unit) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/Button.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Button.kt similarity index 97% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/Button.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Button.kt index f51b6422..cd2cc30d 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/Button.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Button.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/format/Formatters.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ComposeExtensions.kt similarity index 88% rename from src/android/app/src/main/java/info/cemu/cemu/common/format/Formatters.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ComposeExtensions.kt index 0f2f4c7d..5c895954 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/format/Formatters.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ComposeExtensions.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.format +package info.cemu.cemu.common.ui.components import android.text.format.Formatter import androidx.compose.runtime.Composable diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/DrawerLayout.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/DrawerLayout.kt similarity index 96% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/DrawerLayout.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/DrawerLayout.kt index 6d3359f3..d8109bb9 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/DrawerLayout.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/DrawerLayout.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import android.content.Context import android.util.AttributeSet diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/FilledSearchToolbar.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/FilledSearchToolbar.kt similarity index 99% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/FilledSearchToolbar.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/FilledSearchToolbar.kt index 36329b37..64fb4b82 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/FilledSearchToolbar.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/FilledSearchToolbar.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/Header.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Header.kt similarity index 92% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/Header.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Header.kt index 6da3638d..5e0bd7ae 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/Header.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Header.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/ScreenContent.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ScreenContent.kt similarity index 99% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/ScreenContent.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ScreenContent.kt index c42a7caa..14f0f359 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/ScreenContent.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/ScreenContent.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/SingleSelection.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/SingleSelection.kt similarity index 98% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/SingleSelection.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/SingleSelection.kt index d5645dd9..bc084307 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/SingleSelection.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/SingleSelection.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -30,7 +30,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.localization.tr @Composable fun SingleSelection( diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/Slider.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Slider.kt similarity index 97% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/Slider.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Slider.kt index 9f494e70..6c5e3db8 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/Slider.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Slider.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import androidx.annotation.IntRange import androidx.compose.foundation.layout.Column diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/components/Toggle.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Toggle.kt similarity index 97% rename from src/android/app/src/main/java/info/cemu/cemu/common/components/Toggle.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Toggle.kt index ba89850c..0187fc72 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/components/Toggle.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/components/Toggle.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.components +package info.cemu.cemu.common.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/NativeTranslationHelpers.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/NativeTranslationHelpers.kt new file mode 100644 index 00000000..72e9375f --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/NativeTranslationHelpers.kt @@ -0,0 +1,25 @@ +package info.cemu.cemu.common.ui.localization + +import info.cemu.cemu.nativeinterface.NativeGameTitles.ConsoleRegion +import info.cemu.cemu.nativeinterface.NativeInput.EmulatedControllerType + +fun regionToString(region: Int): String = when (region) { + ConsoleRegion.JPN -> tr("Japan") + ConsoleRegion.USA -> tr("USA") + ConsoleRegion.EUR -> tr("Europe") + ConsoleRegion.AUS_DEPR -> tr("Australia") + ConsoleRegion.CHN -> tr("China") + ConsoleRegion.KOR -> tr("Korea") + ConsoleRegion.TWN -> tr("Taiwan") + ConsoleRegion.AUTO -> tr("Auto") + else -> tr("Many") +} + +fun controllerTypeToString(type: Int) = when (type) { + EmulatedControllerType.DISABLED -> tr("Disabled") + EmulatedControllerType.VPAD -> tr("Wii U GamePad") + EmulatedControllerType.PRO -> tr("Wii U Pro Controller") + EmulatedControllerType.WIIMOTE -> tr("Wiimote") + EmulatedControllerType.CLASSIC -> tr("Wii U Classic Controller") + else -> throw IllegalArgumentException("Invalid controller type: $type") +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/translation/PoFile.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/PoFile.kt similarity index 97% rename from src/android/app/src/main/java/info/cemu/cemu/common/translation/PoFile.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/PoFile.kt index 509f2fd6..ed2e9e3b 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/translation/PoFile.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/PoFile.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.translation +package info.cemu.cemu.common.ui.localization import java.io.InputStream diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/translation/Translation.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/Translation.kt similarity index 96% rename from src/android/app/src/main/java/info/cemu/cemu/common/translation/Translation.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/Translation.kt index 02b0109a..0d345cc2 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/translation/Translation.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/localization/Translation.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.translation +package info.cemu.cemu.common.ui.localization import android.content.Context import android.util.Log @@ -63,8 +63,7 @@ fun getCurrentLocale() = I18n.locale fun setTranslations(context: Context) { val assetTranslations = context.assets.list(TRANSLATIONS_FOLDER)?.filter { language -> - if (language == DEFAULT_LANGUAGE) - return@filter false + if (language == DEFAULT_LANGUAGE) return@filter false context.assets.list("$TRANSLATIONS_FOLDER/$language") ?.contains(TRANSLATIONS_FILE_NAME) diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/theme/Theme.kt b/src/android/app/src/main/java/info/cemu/cemu/common/ui/theme/Theme.kt similarity index 96% rename from src/android/app/src/main/java/info/cemu/cemu/common/theme/Theme.kt rename to src/android/app/src/main/java/info/cemu/cemu/common/ui/theme/Theme.kt index e3b28e5a..bb5e3c39 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/theme/Theme.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/common/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.theme +package info.cemu.cemu.common.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationActivity.kt index e3736567..d67acc04 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationActivity.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationActivity.kt @@ -22,7 +22,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import info.cemu.cemu.BuildConfig -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.databinding.ActivityEmulationBinding import info.cemu.cemu.databinding.LayoutSideMenuCheckboxItemBinding import info.cemu.cemu.databinding.LayoutSideMenuEmulationBinding diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationTextInputEditText.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationTextInputEditText.kt index 8882025f..2e778e0a 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationTextInputEditText.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationTextInputEditText.kt @@ -14,7 +14,7 @@ import com.google.android.material.R import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeSwkbd import java.util.regex.Pattern diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/InputHandler.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/InputHandler.kt index 5e3373a5..ec745496 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/InputHandler.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/InputHandler.kt @@ -2,8 +2,8 @@ package info.cemu.cemu.emulation import android.view.KeyEvent import android.view.MotionEvent -import info.cemu.cemu.common.inputdevice.isGameController -import info.cemu.cemu.common.motionevent.isMotionEventFromJoystickOrGamepad +import info.cemu.cemu.common.android.inputdevice.isGameController +import info.cemu.cemu.common.android.motionevent.isMotionEventFromJoystickOrGamepad import info.cemu.cemu.nativeinterface.NativeInput.onNativeAxis import info.cemu.cemu.nativeinterface.NativeInput.onNativeKey diff --git a/src/android/app/src/main/java/info/cemu/cemu/common/canvas/CanvasExtensions.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/CanvasExtensions.kt similarity index 96% rename from src/android/app/src/main/java/info/cemu/cemu/common/canvas/CanvasExtensions.kt rename to src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/CanvasExtensions.kt index 2409a83c..c9717063 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/common/canvas/CanvasExtensions.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/CanvasExtensions.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.common.canvas +package info.cemu.cemu.emulation.inputoverlay import android.graphics.Canvas import android.graphics.Paint diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/DPadInput.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/DPadInput.kt index c97b21f5..98abf615 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/DPadInput.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/DPadInput.kt @@ -4,8 +4,8 @@ import android.graphics.Canvas import android.graphics.Rect import android.graphics.RectF import android.view.MotionEvent -import info.cemu.cemu.common.canvas.fillCircleWithStroke -import info.cemu.cemu.common.canvas.fillRoundRectangleWithStroke +import info.cemu.cemu.emulation.inputoverlay.fillCircleWithStroke +import info.cemu.cemu.emulation.inputoverlay.fillRoundRectangleWithStroke import info.cemu.cemu.emulation.inputoverlay.Colors import info.cemu.cemu.emulation.inputoverlay.OverlayDpad import kotlin.math.atan2 diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/Joystick.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/Joystick.kt index 22aeb59c..c2e4c91d 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/Joystick.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/Joystick.kt @@ -5,7 +5,7 @@ import android.graphics.Paint import android.graphics.Rect import android.view.MotionEvent import info.cemu.cemu.emulation.inputoverlay.Colors -import info.cemu.cemu.common.canvas.fillCircleWithStroke +import info.cemu.cemu.emulation.inputoverlay.fillCircleWithStroke import kotlin.math.min import kotlin.math.sqrt diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/RectangleButton.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/RectangleButton.kt index 7b7aa7e5..03d5c706 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/RectangleButton.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/RectangleButton.kt @@ -3,7 +3,7 @@ package info.cemu.cemu.emulation.inputoverlay.inputs import android.graphics.Canvas import android.graphics.Rect import info.cemu.cemu.emulation.inputoverlay.inputs.innerdrawing.ButtonInnerDrawing -import info.cemu.cemu.common.canvas.fillRoundRectangleWithStroke +import info.cemu.cemu.emulation.inputoverlay.fillRoundRectangleWithStroke class RectangleButton( private val innerDrawing: ButtonInnerDrawing, diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/RoundButton.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/RoundButton.kt index 760fd0d6..157e38e6 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/RoundButton.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/inputoverlay/inputs/RoundButton.kt @@ -3,7 +3,7 @@ package info.cemu.cemu.emulation.inputoverlay.inputs import android.graphics.Canvas import android.graphics.Rect import info.cemu.cemu.emulation.inputoverlay.inputs.innerdrawing.ButtonInnerDrawing -import info.cemu.cemu.common.canvas.fillCircleWithStroke +import info.cemu.cemu.emulation.inputoverlay.fillCircleWithStroke import kotlin.math.min class RoundButton( diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt index 4c33b2a4..15aabdfd 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt @@ -10,9 +10,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.nativeenummapper.regionToString -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.localization.regionToString +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeGameTitles.Game import java.time.LocalDate import java.time.format.DateTimeFormatter diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt index d2d66ed6..8752fcfd 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import info.cemu.cemu.R diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt index 3309d61a..a7f8cec8 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt @@ -5,11 +5,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import info.cemu.cemu.common.components.Header -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.components.SingleSelection -import info.cemu.cemu.common.components.Toggle -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.Header +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.components.SingleSelection +import info.cemu.cemu.common.ui.components.Toggle +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeGameTitles @Composable 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 1e0064e5..3c550b52 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 @@ -56,8 +56,8 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import info.cemu.cemu.R -import info.cemu.cemu.common.components.FilledSearchToolbar -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.FilledSearchToolbar +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeGameTitles import info.cemu.cemu.nativeinterface.NativeGameTitles.Game import kotlinx.coroutines.delay 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/GraphicPackSectionNode.kt index f6fc6bb2..e7e85117 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/GraphicPackSectionNode.kt @@ -31,16 +31,16 @@ class GraphicPackSectionNode : GraphicPackNode { graphicPackBasicInfo: NativeGraphicPacks.GraphicPackBasicInfo, titleIdInstalled: Boolean ) { - var node = this; + var node = this val tokens = graphicPackBasicInfo.virtualPath.split("/") if (tokens.isEmpty()) { - return; + return } for (token in tokens.subList(0, tokens.size - 1)) { - node = getOrAddSectionByToken(node, token, node.children, titleIdInstalled); + node = getOrAddSectionByToken(node, token, node.children, titleIdInstalled) } if (graphicPackBasicInfo.enabled) { - node.updateEnabledCount(true); + node.updateEnabledCount(true) } node.children.add( GraphicPackDataNode( @@ -63,11 +63,11 @@ class GraphicPackSectionNode : GraphicPackNode { val existingSectionNode = graphicPackNodes.firstOrNull { it is GraphicPackSectionNode && it.name == token } if (existingSectionNode != null) { - return existingSectionNode as GraphicPackSectionNode; + return existingSectionNode as GraphicPackSectionNode } - val sectionNode = GraphicPackSectionNode(token, titleIdInstalled, parent); - graphicPackNodes.add(sectionNode); - return sectionNode; + val sectionNode = GraphicPackSectionNode(token, titleIdInstalled, parent) + graphicPackNodes.add(sectionNode) + return sectionNode } fun sort() { 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 index c46d74f5..46d2d849 100644 --- 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 @@ -7,6 +7,7 @@ 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 @@ -36,7 +37,6 @@ class GraphicPacksListViewModel : ViewModel() { val downloadStatus = _downloadStatus.asStateFlow() - @Volatile private var downloadJob: Job? = null fun downloadNewUpdate(context: Context) { if (_downloadStatus.value != null) return @@ -44,7 +44,7 @@ class GraphicPacksListViewModel : ViewModel() { try { GraphicPacksDownloader.download(context) { updateDownloadStatus(it) } refreshGraphicPacks() - } catch (exception: Exception) { + } catch (_: Exception) { updateDownloadStatus(GraphicPacksDownloadStatus.ERROR) } } @@ -55,9 +55,10 @@ class GraphicPacksListViewModel : ViewModel() { } fun cancelDownload() { - downloadJob?.cancel(CancellationException("Canceled by user")) + val oldDownloadJob = downloadJob ?: return downloadJob = null viewModelScope.launch { + oldDownloadJob.cancelAndJoin() updateDownloadStatus(GraphicPacksDownloadStatus.CANCELED) } } 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/GraphicPacksScreens.kt index a54164f6..ea7d6227 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/GraphicPacksScreens.kt @@ -55,11 +55,11 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel import info.cemu.cemu.R -import info.cemu.cemu.common.components.DefaultAppBarTitle -import info.cemu.cemu.common.components.ScreenContentLazy -import info.cemu.cemu.common.components.SearchToolbarInput -import info.cemu.cemu.common.components.SingleSelection -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.DefaultAppBarTitle +import info.cemu.cemu.common.ui.components.ScreenContentLazy +import info.cemu.cemu.common.ui.components.SearchToolbarInput +import info.cemu.cemu.common.ui.components.SingleSelection +import info.cemu.cemu.common.ui.localization.tr import kotlinx.coroutines.launch diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeGameTitles.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeGameTitles.kt index c51a8887..777a334e 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeGameTitles.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeGameTitles.kt @@ -232,6 +232,17 @@ object NativeGameTitles { @JvmStatic external fun compressQueuedTitle(fd: Int, compressCallbacks: TitleCompressCallbacks) + enum class CompressResult { + FINISHED, + ERROR, + } + + fun compressQueuedTitle(fd: Int, callback: (CompressResult) -> Unit) = + compressQueuedTitle(fd, object : TitleCompressCallbacks { + override fun onFinished() = callback(CompressResult.FINISHED) + override fun onError() = callback(CompressResult.ERROR) + }) + @JvmStatic external fun getCurrentProgressForCompression(): Long diff --git a/src/android/app/src/main/java/info/cemu/cemu/provider/DocumentsProvider.kt b/src/android/app/src/main/java/info/cemu/cemu/provider/DocumentsProvider.kt index b2ce3a6b..8a76cd3f 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/provider/DocumentsProvider.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/provider/DocumentsProvider.kt @@ -13,7 +13,7 @@ import android.provider.DocumentsProvider import android.webkit.MimeTypeMap import info.cemu.cemu.BuildConfig import info.cemu.cemu.R -import info.cemu.cemu.common.context.internalFolder +import info.cemu.cemu.common.android.context.internalFolder import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt index 4fce2cdc..807cc114 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt @@ -2,9 +2,9 @@ package info.cemu.cemu.settings import androidx.compose.runtime.Composable import androidx.lifecycle.compose.dropUnlessResumed -import info.cemu.cemu.common.components.Button -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.Button +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.localization.tr data class SettingsHomeScreenActions( val goToGeneralSettings: () -> Unit, diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/account/AccountsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/account/AccountsScreen.kt index 3c7bcc75..c5557151 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/account/AccountsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/account/AccountsScreen.kt @@ -47,11 +47,11 @@ import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import info.cemu.cemu.R -import info.cemu.cemu.common.components.Header -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.components.SingleSelection -import info.cemu.cemu.common.translation.getCurrentLocale -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.Header +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.components.SingleSelection +import info.cemu.cemu.common.ui.localization.getCurrentLocale +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeAccount import info.cemu.cemu.nativeinterface.NativeAccount.AccountGender import info.cemu.cemu.nativeinterface.NativeAccount.DEFAULT_MII_NAME diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt index 99557a22..79a58ef8 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt @@ -1,11 +1,11 @@ package info.cemu.cemu.settings.audio import androidx.compose.runtime.Composable -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.components.SingleSelection -import info.cemu.cemu.common.components.Slider -import info.cemu.cemu.common.components.Toggle -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.components.SingleSelection +import info.cemu.cemu.common.ui.components.Slider +import info.cemu.cemu.common.ui.components.Toggle +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeSettings private const val AUDIO_LATENCY_STEPS = 22 diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt index e0783533..f01c4f1f 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel -import info.cemu.cemu.common.components.ScreenContentLazy -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ScreenContentLazy +import info.cemu.cemu.common.ui.localization.tr import kotlinx.coroutines.launch @Composable diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt index 7a59f60a..1b1140a2 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt @@ -28,8 +28,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.viewmodel.compose.viewModel -import info.cemu.cemu.common.components.ScreenContentLazy -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ScreenContentLazy +import info.cemu.cemu.common.ui.localization.tr import kotlinx.coroutines.launch @Composable diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt index 03c4dc4a..2aa7c11d 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt @@ -4,11 +4,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel -import info.cemu.cemu.common.components.Button -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.components.SingleSelection -import info.cemu.cemu.common.components.Toggle -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.Button +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.components.SingleSelection +import info.cemu.cemu.common.ui.components.Toggle +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeSettings @Composable diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsViewModel.kt index 4356f3e9..d70f9de8 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsViewModel.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel import info.cemu.cemu.common.settings.EmulationScreenSettings import info.cemu.cemu.common.settings.GuiSettings import info.cemu.cemu.common.settings.SettingsManager -import info.cemu.cemu.common.translation.getAvailableLanguages +import info.cemu.cemu.common.ui.localization.getAvailableLanguages class GeneralSettingsViewModel : ViewModel() { val languages: List @@ -20,7 +20,7 @@ class GeneralSettingsViewModel : ViewModel() { } fun setLanguage(language: String, context: Context) { - info.cemu.cemu.common.translation.setLanguage(language, context) + info.cemu.cemu.common.ui.localization.setLanguage(language, context) guiSettings.language = language } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt index 2e7a4102..c8ea9b5e 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt @@ -2,11 +2,11 @@ package info.cemu.cemu.settings.graphics import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.rememberSaveable -import info.cemu.cemu.common.components.Button -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.components.SingleSelection -import info.cemu.cemu.common.components.Toggle -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.Button +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.components.SingleSelection +import info.cemu.cemu.common.ui.components.Toggle +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeEmulation import info.cemu.cemu.nativeinterface.NativeSettings diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt index 5c425f44..da076e79 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt @@ -1,7 +1,7 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeInput.ClassicButton @Composable diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt index a3ed29fe..8a145db8 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt @@ -34,10 +34,10 @@ import androidx.compose.ui.window.Dialog import androidx.lifecycle.viewmodel.MutableCreationExtras import androidx.lifecycle.viewmodel.compose.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.components.SingleSelection -import info.cemu.cemu.common.nativeenummapper.controllerTypeToString -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.components.SingleSelection +import info.cemu.cemu.common.ui.localization.controllerTypeToString +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeInput import kotlinx.coroutines.launch import androidx.compose.material3.Button as MaterialButton diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt index 8426ce9f..4ba2a5db 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import info.cemu.cemu.common.components.Header +import info.cemu.cemu.common.ui.components.Header @Composable fun InputItemsGroup( diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputMapper.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputMapper.kt index 0b167f59..b7cfec0c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputMapper.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputMapper.kt @@ -3,16 +3,13 @@ package info.cemu.cemu.settings.input import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent -import info.cemu.cemu.common.inputdevice.isGameController -import info.cemu.cemu.common.motionevent.isMotionEventFromJoystickOrGamepad +import info.cemu.cemu.common.android.inputdevice.isGameController +import info.cemu.cemu.common.android.motionevent.isMotionEventFromJoystickOrGamepad import info.cemu.cemu.nativeinterface.NativeInput import info.cemu.cemu.nativeinterface.NativeInput.setControllerMapping import kotlin.math.abs -private class InvalidAxisException(axis: Int) : Exception("Invalid axis $axis") - -@Throws(InvalidAxisException::class) -private fun getNativeAxisKey(axis: Int, isPositive: Boolean): Int { +private fun getNativeAxisKey(axis: Int, isPositive: Boolean): Int? { return if (isPositive) { when (axis) { MotionEvent.AXIS_X -> NativeInput.Axis.X_POS @@ -23,7 +20,7 @@ private fun getNativeAxisKey(axis: Int, isPositive: Boolean): Int { MotionEvent.AXIS_RTRIGGER -> NativeInput.Axis.TRIGGER_Y_POS MotionEvent.AXIS_HAT_X -> NativeInput.Axis.DPAD_RIGHT MotionEvent.AXIS_HAT_Y -> NativeInput.Axis.DPAD_DOWN - else -> throw InvalidAxisException(axis) + else -> null } } else { when (axis) { @@ -33,7 +30,7 @@ private fun getNativeAxisKey(axis: Int, isPositive: Boolean): Int { MotionEvent.AXIS_RY, MotionEvent.AXIS_RZ -> NativeInput.Axis.ROTATION_Y_NEG MotionEvent.AXIS_HAT_X -> NativeInput.Axis.DPAD_LEFT MotionEvent.AXIS_HAT_Y -> NativeInput.Axis.DPAD_UP - else -> throw InvalidAxisException(axis) + else -> null } } } @@ -55,12 +52,7 @@ object InputMapper { val actionPointerIndex = event.actionIndex for (motionRange in device.motionRanges) { val axisValue = event.getAxisValue(motionRange.axis, actionPointerIndex) - var axis: Int - try { - axis = getNativeAxisKey(motionRange.axis, axisValue > 0) - } catch (_: InvalidAxisException) { - continue - } + val axis = getNativeAxisKey(motionRange.axis, axisValue > 0) ?: continue if (abs(axisValue.toDouble()) > maxAbsAxisValue) { maxAxis = axis maxAbsAxisValue = abs(axisValue.toDouble()).toFloat() @@ -99,12 +91,13 @@ object InputMapper { axisCode: Int, isPositive: Boolean, ) { + val axis = getNativeAxisKey(axisCode, isPositive) ?: return setControllerMapping( deviceDescriptor, deviceName, controllerIndex, mappingId, - getNativeAxisKey(axisCode, isPositive) + axis ) } diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt index d11dceca..5b8ccd33 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt @@ -3,10 +3,10 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.lifecycle.compose.dropUnlessResumed -import info.cemu.cemu.common.components.Button -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.nativeenummapper.controllerTypeToString -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.Button +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.localization.controllerTypeToString +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeInput data class InputSettingsScreenActions( @@ -29,8 +29,7 @@ fun InputSettingsScreen(navigateBack: () -> Unit, actions: InputSettingsScreenAc label = tr("Input overlay settings"), onClick = dropUnlessResumed { actions.goToInputOverlaySettings() }, ) - controllers.forEach { controllerTypePair -> - val (controllerIndex, controllerEmulatedType) = controllerTypePair + controllers.forEach { (controllerIndex, controllerEmulatedType) -> Button( label = tr("Controller {0}", controllerIndex + 1), description = tr( diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt index ee552906..e5f3aee3 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt @@ -1,7 +1,7 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeInput.ProButton @Composable diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt index aadf3ed9..5b92687e 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt @@ -1,8 +1,8 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable -import info.cemu.cemu.common.components.Toggle -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.Toggle +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeInput import info.cemu.cemu.nativeinterface.NativeInput.VPADButton diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt index 25068971..fcf4af8c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt @@ -1,7 +1,7 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeInput.WiimoteButton @Composable diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt index 9dcdf21a..c3590125 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt @@ -2,11 +2,11 @@ package info.cemu.cemu.settings.inputoverlay import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel -import info.cemu.cemu.common.components.ScreenContent -import info.cemu.cemu.common.components.SingleSelection -import info.cemu.cemu.common.components.Slider -import info.cemu.cemu.common.components.Toggle -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ScreenContent +import info.cemu.cemu.common.ui.components.SingleSelection +import info.cemu.cemu.common.ui.components.Slider +import info.cemu.cemu.common.ui.components.Toggle +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeInput private val ControllerIndexChoices = (0.., ) -interface TitleDeleteCallbacks { - fun onDeleteFinished() - fun onError() -} - -interface TitleInstallCallbacks { - fun onInstallFinished() - fun onError() -} - -class FilterActions( - private val currentFilters: () -> Set, - private val updateFilters: (Set) -> Unit, -) { - fun add(filter: T) { - updateFilters(currentFilters() + filter) - } - - fun remove(filter: T) { - updateFilters(currentFilters() - filter) - } -} - class TitleListViewModel : ViewModel() { private val mlcPath = Path(NativeActiveSettings.getMLCPath()) + private val installUseCase = InstallTitleUseCase(viewModelScope, mlcPath) + private val deleteUseCase = DeleteTitleUseCase(viewModelScope) + private val compressUseCase = CompressTitleUseCase(viewModelScope) + private fun isPathInMLC(path: String): Boolean = Path(path).relativeToOrNull(mlcPath)?.let { it.startsWith("sys") || it.startsWith("usr") } ?: false @@ -89,24 +56,25 @@ class TitleListViewModel : ViewModel() { ) val filter = _filter.asStateFlow() - fun setFilterQuery(query: String) { - _filter.value = _filter.value.copy(query = query) + private fun updateFilter(transform: (TitleListFilter) -> TitleListFilter) { + _filter.value = transform(_filter.value) } - val typesFilter = - FilterActions( - { _filter.value.types }, - { _filter.value = _filter.value.copy(types = it) }) + fun setFilterQuery(query: String) { + updateFilter { it.copy(query = query) } + } - val formatsFilter = - FilterActions( - { _filter.value.formats }, - { _filter.value = _filter.value.copy(formats = it) }) + fun toggleType(type: EntryType) { + updateFilter { it.copy(types = it.types.toggleInSet(type)) } + } - val pathsFilter = - FilterActions( - { _filter.value.paths }, - { _filter.value = _filter.value.copy(paths = it) }) + fun toggleFormat(format: EntryFormat) { + updateFilter { it.copy(formats = it.formats.toggleInSet(format)) } + } + + fun togglePath(path: EntryPath) { + updateFilter { it.copy(paths = it.paths.toggleInSet(path)) } + } private val _titleEntries = MutableStateFlow>(emptyList()) val titleEntries = filter.combine(_titleEntries) { filter, entries -> @@ -175,33 +143,31 @@ class TitleListViewModel : ViewModel() { fun deleteTitleEntry( titleEntry: TitleEntry, context: Context, - deleteCallbacks: TitleDeleteCallbacks, + callback: (DeleteResult) -> Unit, ) { if (_titleToBeDeleted.value != null) return - if (!_titleEntries.value.any { it.locationUID == titleEntry.locationUID }) { - deleteCallbacks.onDeleteFinished() + if (!_titleEntries.value.any { it.locationUID == titleEntry.locationUID }) return - } _titleToBeDeleted.value = titleEntry - viewModelScope.launch(Dispatchers.IO) { - try { - if (!delete(context.contentResolver, titleEntry.path)) { - deleteCallbacks.onError() - return@launch + deleteUseCase.delete( + context = context, + titleEntry = titleEntry, + callback = { result -> + if (result == DeleteResult.FINISHED) { + _titleEntries.value = _titleEntries.value.filterNot { + it.locationUID == titleEntry.locationUID && it.path == titleEntry.path + } } - _titleEntries.value = - _titleEntries.value.filter { it.locationUID != titleEntry.locationUID && it.path != titleEntry.path } - deleteCallbacks.onDeleteFinished() - } catch (e: Exception) { - deleteCallbacks.onError() - } finally { + _titleToBeDeleted.value = null + + callback(result) } - } + ) } private val _queuedTitleToInstall = @@ -230,159 +196,23 @@ class TitleListViewModel : ViewModel() { _queuedTitleToInstall.value = null } - private var installTitleJob: Job? = null - fun cancelInstall() { - installTitleJob?.cancel() - installTitleJob = null - } - private suspend fun listFilesInSourceDirs( - contentResolver: ContentResolver, - titleDir: DocumentFile, - titleUri: Uri, - targetLocation: String, - ): Pair> { - val entries = LinkedList() - var totalSize = 0L + val titleInstallInProgress = installUseCase.inProgress + val titleInstallProgress = installUseCase.progress - for (sourceDir in SOURCE_DIRS) { - val parentUri = titleDir.findFile(sourceDir)!!.uri - val parentUriLength = titleUri.toString().length - val uriToTargetPath: (Uri) -> Path = { - val relativePath = it.toString().substring(parentUriLength).urlDecode() - Path(targetLocation, relativePath) - } - - entries += DirEntry.Dir(Path(targetLocation, sourceDir)) - listFilesRecursively( - contentResolver = contentResolver, - dirUri = parentUri, - onFile = { uri, sizeInBytes -> - totalSize += sizeInBytes - entries += DirEntry.File( - uri, - uriToTargetPath(uri), - sizeInBytes - ) - }, - onDir = { entries += DirEntry.Dir(uriToTargetPath(it)) }, - ) - } - - totalSize = max(totalSize, 1L) - - return Pair(totalSize, entries) - } - - private val _titleInstallInProgress = MutableStateFlow(false) - val titleInstallInProgress = _titleInstallInProgress.asStateFlow() - - private val _titleInstallProgress = MutableStateFlow?>(null) - val titleInstallProgress = _titleInstallProgress.asStateFlow() - - fun installQueuedTitle( - context: Context, - titleInstallCallbacks: TitleInstallCallbacks, - ) { - if (_titleInstallInProgress.value) - return - - val titleToInstall = _queuedTitleToInstall.value - if (titleToInstall == null) { - titleInstallCallbacks.onInstallFinished() - return - } + fun installQueuedTitle(context: Context, callback: (InstallResult) -> Unit) { + val queued = _queuedTitleToInstall.value ?: return _queuedTitleToInstall.value = null - val titleUri = titleToInstall.first - val targetLocation = titleToInstall.second.targetLocation - - val installPath = Path(targetLocation) - val installFile = installPath.toFile() - val backupFile = installPath.getBackupFile() - - _titleInstallProgress.value = null - _titleInstallInProgress.value = true - - val oldInstallJob = installTitleJob - installTitleJob = viewModelScope.launch(Dispatchers.IO) { - var installStarted = false - try { - cleanupJob?.join() - oldInstallJob?.join() - val contentResolver = context.contentResolver - val buffer = ByteArray(8192) - - val (totalSize, entries) = listFilesInSourceDirs( - contentResolver = contentResolver, - titleDir = DocumentFile.fromTreeUri(context, titleUri)!!, - titleUri = titleUri, - targetLocation = targetLocation, - ) - - if (totalSize > mlcPath.toFile().freeSpace) { - titleInstallCallbacks.onError() - return@launch - } - - backupFile.deleteRecursively() - if (installFile.exists()) - installFile.renameTo(backupFile) - - installStarted = true - _titleInstallProgress.value = Pair(0, totalSize) - var bytesWritten = 0L - - for (file in entries) { - yield() - when (file) { - is DirEntry.Dir -> { - file.destinationPath.createDirectories() - } - - is DirEntry.File -> contentResolver.openInputStream(file.uri)?.use { - copyInputStreamToFile(it, file.destinationPath, buffer) - bytesWritten += file.sizeInBytes - _titleInstallProgress.value = Pair(bytesWritten, totalSize) - } - } - } - - if (backupFile.exists()) - backupFile.deleteRecursively() - - NativeGameTitles.addTitleFromPath(targetLocation) - titleInstallCallbacks.onInstallFinished() - } catch (exception: Exception) { - if (installStarted) - cleanupInstall(installFile) - - if (exception !is CancellationException) - titleInstallCallbacks.onError() - } finally { - _titleInstallInProgress.value = false - } - } + installUseCase.install( + context = context, + titleUri = queued.first, + targetLocation = queued.second.targetLocation, + callback = callback, + ) } - private var cleanupJob: Job? = null - private fun cleanupInstall(installFile: File) { - val oldCleanupJob = cleanupJob - cleanupJob = viewModelScope.launch(Dispatchers.IO) { - oldCleanupJob?.join() - val installPath = installFile.toPath() - var tempFile: File? = null - if (installFile.exists()) { - val tempName = "${installPath.fileName}-${Random.nextUInt()}" - tempFile = installPath.resolveSibling(tempName).toFile() - installFile.renameTo(tempFile!!) - } - val backupInstall = installPath.getBackupFile() - if (backupInstall.exists()) - backupInstall.renameTo(installFile) - tempFile?.deleteRecursively() - } - } + fun cancelInstall() = installUseCase.cancel() private val _queuedTitleToCompress = MutableStateFlow(null) @@ -407,88 +237,28 @@ class TitleListViewModel : ViewModel() { _queuedTitleToCompress.value = null } - private val _compressProgress = MutableStateFlow(null) - val compressProgress = _compressProgress.asStateFlow() - private var compressProgressJob: Job? = null + val compressInProgress = compressUseCase.inProgress + val compressProgress = compressUseCase.progress - private val _compressInProgress = MutableStateFlow(false) - val compressInProgress = _compressInProgress.asStateFlow() fun compressQueuedTitle( context: Context, uri: Uri, - onFinished: () -> Unit, - onError: () -> Unit, + onResult: (CompressResult) -> Unit ) { - if (_compressInProgress.value) - return - - _queuedTitleToCompress.value = null - - val fd = context.contentResolver.openFileDescriptor(uri, "rw") ?: return - - val oldCompressProgressJob = compressProgressJob - compressProgressJob = viewModelScope.launch { - oldCompressProgressJob?.cancelAndJoin() - _compressInProgress.value = true - try { - while (true) { - delay(500) - _compressProgress.value = NativeGameTitles.getCurrentProgressForCompression() - } - } catch (_: CancellationException) { - } finally { - _compressInProgress.value = false - _compressProgress.value = null - } - } - - NativeGameTitles.compressQueuedTitle( - fd = fd.detachFd(), - compressCallbacks = object : NativeGameTitles.TitleCompressCallbacks { - override fun onFinished() { - compressProgressJob?.cancel() - onFinished() - } - - override fun onError() { - compressProgressJob?.cancel() - onError() - } - }) + compressUseCase.compress(context, uri, onResult) } - fun cancelCompression() { - viewModelScope.launch(Dispatchers.IO) { - compressProgressJob?.cancelAndJoin() - NativeGameTitles.cancelTitleCompression() - } - } + fun cancelCompression() = compressUseCase.cancel() fun getCompressedFileNameForQueuedTitle() = NativeGameTitles.getCompressedFileNameForQueuedTitle() companion object { private const val REFRESH_DEBOUNCE_TIME_MILLISECONDS = 1500L - private val SOURCE_DIRS = arrayOf( - "content", - "code", - "meta", - ) } } -private fun delete(contentResolver: ContentResolver, path: String): Boolean { - if (path.isContentUri()) { - return DocumentsContract.deleteDocument( - contentResolver, - path.fromNativePath() - ) - } - - return Path(path).toFile().deleteRecursively() -} - private fun NativeGameTitles.SaveData.toTitleEntry(isInMlc: Boolean) = TitleEntry( titleId = this.titleId, name = this.name, @@ -512,57 +282,3 @@ private fun NativeGameTitles.TitleData.toTitleEntry(isInMlc: Boolean) = TitleEnt type = nativeTitleTypeToEnum(this.titleType), format = nativeTitleFormatToEnum(this.titleDataFormat), ) - -private fun Path.getBackupFile() = resolveSibling("$fileName.backup").toFile() - -private sealed class DirEntry { - data class File(val uri: Uri, val destinationPath: Path, val sizeInBytes: Long) : DirEntry() - data class Dir(val destinationPath: Path) : DirEntry() -} - -private suspend fun listFilesRecursively( - contentResolver: ContentResolver, - dirUri: Uri, - onFile: (Uri, Long) -> Unit, - onDir: (Uri) -> Unit, -) { - val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - dirUri, - DocumentsContract.getDocumentId(dirUri) - ) - val cursor = contentResolver.query( - childrenUri, - arrayOf( - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_MIME_TYPE, - ), - null, - null, - null - ) - cursor?.use { - while (it.moveToNext()) { - yield() - - val documentId = it.getString(0) - val documentUri = DocumentsContract.buildDocumentUriUsingTree(dirUri, documentId) - - val mimeType = it.getString(2) - if (mimeType != DocumentsContract.Document.MIME_TYPE_DIR) { - val sizeInBytes = it.getLong(1) - onFile(documentUri, sizeInBytes) - continue - } - - onDir(documentUri) - - listFilesRecursively( - contentResolver, - DocumentsContract.buildDocumentUriUsingTree(dirUri, documentId), - onFile, - onDir, - ) - } - } -} 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 4c4a2421..efd3f9bb 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,7 +51,6 @@ 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 @@ -65,11 +64,14 @@ import androidx.compose.ui.unit.sp import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.viewmodel.compose.viewModel import info.cemu.cemu.R -import info.cemu.cemu.common.components.ScreenContentLazy -import info.cemu.cemu.common.format.formatBytes -import info.cemu.cemu.common.nativeenummapper.regionToString -import info.cemu.cemu.common.translation.tr +import info.cemu.cemu.common.ui.components.ScreenContentLazy +import info.cemu.cemu.common.ui.components.formatBytes +import info.cemu.cemu.common.ui.localization.regionToString +import info.cemu.cemu.common.ui.localization.tr import info.cemu.cemu.nativeinterface.NativeGameTitles +import info.cemu.cemu.titlemanager.usecases.CompressResult +import info.cemu.cemu.titlemanager.usecases.DeleteResult +import info.cemu.cemu.titlemanager.usecases.InstallResult import kotlinx.coroutines.launch import java.text.MessageFormat @@ -101,15 +103,12 @@ fun TitleManagerScreen( fun installQueuedTitle() { titleListViewModel.installQueuedTitle( context = context, - titleInstallCallbacks = object : TitleInstallCallbacks { - override fun onInstallFinished() { - showNotificationMessage(tr("Finished installing")) + callback = { + when (it) { + InstallResult.ERROR -> showNotificationMessage(tr("Error installing")) + InstallResult.FINISHED -> showNotificationMessage(tr("Finished installing")) } - - override fun onError() { - showNotificationMessage(tr("Error installing")) - } - } + }, ) } @@ -135,11 +134,13 @@ fun TitleManagerScreen( titleListViewModel.compressQueuedTitle( context = context, uri = uri, - onFinished = { - showNotificationMessage(tr("Finished converting")) - }, - onError = { - showNotificationMessage(tr("Error while converting")) + onResult = { result -> + { + when (result) { + CompressResult.FINISHED -> showNotificationMessage(tr("Finished converting")) + CompressResult.ERROR -> showNotificationMessage(tr("Error while converting")) + } + } } ) } @@ -179,13 +180,10 @@ fun TitleManagerScreen( titleListViewModel.deleteTitleEntry( titleEntry = it, context = context, - deleteCallbacks = object : TitleDeleteCallbacks { - override fun onDeleteFinished() { - showNotificationMessage(tr("Deleted title entry")) - } - - override fun onError() { - showNotificationMessage(tr("Failed to delete title entry")) + callback = { result -> + when (result) { + DeleteResult.FINISHED -> showNotificationMessage(tr("Deleted title entry")) + DeleteResult.ERROR -> showNotificationMessage(tr("Failed to delete title entry")) } }) }, @@ -481,34 +479,30 @@ private fun TitleFilterBottomSheet( filterRowLabel = tr("Types"), filterValues = EntryType.entries.map { (it to (it in filter.types)) }, valueToLabel = { entryTypeToString(it) }, - onFilterAdded = titleListViewModel.typesFilter::add, - onFilterRemoved = titleListViewModel.typesFilter::remove, + onToggle = titleListViewModel::toggleType, ) FilterRow( filterRowLabel = tr("Formats"), filterValues = EntryFormat.entries.map { (it to (it in filter.formats)) }, valueToLabel = { formatToString(it) }, - onFilterAdded = titleListViewModel.formatsFilter::add, - onFilterRemoved = titleListViewModel.formatsFilter::remove, + onToggle = titleListViewModel::toggleFormat, ) FilterRow( filterRowLabel = tr("Locations"), filterValues = EntryPath.entries.map { (it to (it in filter.paths)) }, valueToLabel = { pathToString(it) }, - onFilterAdded = titleListViewModel.pathsFilter::add, - onFilterRemoved = titleListViewModel.pathsFilter::remove, + onToggle = titleListViewModel::togglePath, ) } } } @Composable -private fun FilterRow( +private fun > FilterRow( filterRowLabel: String, filterValues: List>, valueToLabel: @Composable (T) -> String, - onFilterAdded: (T) -> Unit, - onFilterRemoved: (T) -> Unit, + onToggle: (T) -> Unit ) { var showOptions by remember { mutableStateOf(false) } Column( @@ -536,8 +530,7 @@ private fun FilterRow( FilterChip( label = valueToLabel(value), selected = selected, - onAdd = { onFilterAdded(value) }, - onRemove = { onFilterRemoved(value) }, + onToggle = { onToggle(value) }, ) } } @@ -545,7 +538,7 @@ private fun FilterRow( } @Composable -fun FilterChip(label: String, selected: Boolean, onAdd: () -> Unit, onRemove: () -> Unit) { +fun FilterChip(label: String, selected: Boolean, onToggle: () -> Unit) { FilterChip( leadingIcon = { if (selected) @@ -556,8 +549,7 @@ fun FilterChip(label: String, selected: Boolean, onAdd: () -> Unit, onRemove: () }, selected = selected, onClick = { - if (selected) onRemove() - else onAdd() + onToggle() }, label = { Text(label) } ) @@ -569,7 +561,7 @@ private fun TitleEntryListItem( onDeleteRequest: () -> Unit, onCompressRequested: () -> Unit, ) { - var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } + var showDeleteConfirmationDialog by remember { mutableStateOf(false) } Card( colors = CardDefaults.cardColors( 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 new file mode 100644 index 00000000..3cb2824b --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/CompressTitleUseCase.kt @@ -0,0 +1,85 @@ +package info.cemu.cemu.titlemanager.usecases + +import android.content.Context +import android.net.Uri +import info.cemu.cemu.nativeinterface.NativeGameTitles +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +private typealias NativeCompressResult = NativeGameTitles.CompressResult + +enum class CompressResult { + FINISHED, + ERROR, +} + +class CompressTitleUseCase(private val scope: CoroutineScope) { + private val _inProgress = MutableStateFlow(false) + val inProgress = _inProgress.asStateFlow() + + private val _progress = MutableStateFlow(null) + val progress = _progress.asStateFlow() + + private var compressJob: Job? = null + private var progressJob: Job? = null + + + fun cancel() { + scope.launch(Dispatchers.IO) { + progressJob?.cancelAndJoin() + NativeGameTitles.cancelTitleCompression() + } + } + + fun compress( + context: Context, + uri: Uri, + callback: (CompressResult) -> Unit + ) { + if (_inProgress.value) return + + val fd = context.contentResolver.openFileDescriptor(uri, "rw") + + if (fd == null) { + callback(CompressResult.ERROR) + return + } + + val oldProgressJob = progressJob + compressJob = scope.launch { + oldProgressJob?.cancelAndJoin() + _inProgress.value = true + + try { + progressJob = launch { + while (isActive) { + delay(500) + _progress.value = NativeGameTitles.getCurrentProgressForCompression() + } + } + + NativeGameTitles.compressQueuedTitle( + fd = fd.detachFd(), + callback = { result -> + when (result) { + NativeCompressResult.FINISHED -> callback(CompressResult.FINISHED) + NativeCompressResult.ERROR -> callback(CompressResult.ERROR) + } + + progressJob?.cancel() + _inProgress.value = false + _progress.value = null + } + ) + } catch (_: Exception) { + progressJob?.cancel() + + _inProgress.value = false + _progress.value = null + + callback(CompressResult.ERROR) + } + } + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/DeleteTitleUseCase.kt b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/DeleteTitleUseCase.kt new file mode 100644 index 00000000..03a0eb1f --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/DeleteTitleUseCase.kt @@ -0,0 +1,50 @@ +package info.cemu.cemu.titlemanager.usecases + +import android.content.ContentResolver +import android.content.Context +import android.provider.DocumentsContract +import info.cemu.cemu.common.string.isContentUri +import info.cemu.cemu.nativeinterface.fromNativePath +import info.cemu.cemu.titlemanager.TitleEntry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlin.io.path.Path + +enum class DeleteResult +{ + FINISHED, + ERROR, +} + +class DeleteTitleUseCase( + private val scope: CoroutineScope +) { + fun delete( + context: Context, + titleEntry: TitleEntry, + callback: (DeleteResult) -> Unit, + ) { + scope.launch(Dispatchers.IO) { + try { + if (delete(context.contentResolver, titleEntry.path)) + callback(DeleteResult.FINISHED) + else + callback(DeleteResult.ERROR) + } catch (_: Exception) { + callback(DeleteResult.ERROR) + } + } + } + + private fun delete(contentResolver: ContentResolver, path: String): Boolean { + if (path.isContentUri()) { + return DocumentsContract.deleteDocument( + contentResolver, + path.fromNativePath() + ) + } + + return Path(path).toFile().deleteRecursively() + } +} 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 new file mode 100644 index 00000000..c4d0e52d --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/usecases/InstallTitleUseCase.kt @@ -0,0 +1,199 @@ +package info.cemu.cemu.titlemanager.usecases + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import info.cemu.cemu.common.android.contentresolver.DocumentEntry +import info.cemu.cemu.common.android.contentresolver.walkDocumentTree +import info.cemu.cemu.common.io.copyInputStreamToFile +import info.cemu.cemu.common.string.urlDecode +import info.cemu.cemu.nativeinterface.NativeGameTitles +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield +import java.io.File +import java.nio.file.Path +import java.util.LinkedList +import kotlin.coroutines.cancellation.CancellationException +import kotlin.io.path.Path +import kotlin.io.path.createDirectories +import kotlin.math.max +import kotlin.random.Random +import kotlin.random.nextUInt + +enum class InstallResult{ + ERROR, + FINISHED, +} + +private sealed class DirEntry { + data class File(val uri: Uri, val destinationPath: Path, val size: Long) : DirEntry() + data class Dir(val destinationPath: Path) : DirEntry() +} + +class InstallTitleUseCase( + private val scope: CoroutineScope, + private val mlcPath: Path +) { + private val _inProgress = MutableStateFlow(false) + val inProgress: StateFlow = _inProgress + + private val _progress = MutableStateFlow?>(null) + val progress: StateFlow?> = _progress + + private var installJob: Job? = null + private var cleanupJob: Job? = null + + fun cancel() { + installJob?.cancel() + installJob = null + } + + fun install( + context: Context, + titleUri: Uri, + targetLocation: String, + callback: (InstallResult) -> Unit + ) { + if (_inProgress.value) return + + val installPath = Path(targetLocation) + val installFile = installPath.toFile() + val backupFile = installPath.getBackupFile() + + _progress.value = null + _inProgress.value = true + + val oldInstallJob = installJob + installJob = scope.launch(Dispatchers.IO) { + var installStarted = false + + try { + cleanupJob?.join() + oldInstallJob?.join() + val contentResolver = context.contentResolver + val buffer = ByteArray(8192) + + val (totalSize, entries) = listFilesInSourceDirs( + contentResolver = contentResolver, + titleDir = DocumentFile.fromTreeUri(context, titleUri)!!, + titleUri = titleUri, + targetLocation = targetLocation, + ) + + if (totalSize > mlcPath.toFile().freeSpace) { + callback(InstallResult.ERROR) + return@launch + } + + backupFile.deleteRecursively() + if (installFile.exists()) + installFile.renameTo(backupFile) + + installStarted = true + _progress.value = 0L to totalSize + var bytesWritten = 0L + + for (file in entries) { + yield() + when (file) { + is DirEntry.Dir -> file.destinationPath.createDirectories() + is DirEntry.File -> contentResolver.openInputStream( + file.uri + )?.use { + copyInputStreamToFile(it, file.destinationPath, buffer) + bytesWritten += file.size + _progress.value = bytesWritten to totalSize + } + } + } + + if (backupFile.exists()) + backupFile.deleteRecursively() + + NativeGameTitles.addTitleFromPath(targetLocation) + + callback(InstallResult.FINISHED) + } catch (e: Exception) { + if (installStarted) + cleanupInstall(installFile) + + if (e !is CancellationException) callback(InstallResult.ERROR) + } finally { + _inProgress.value = false + } + } + } + + private suspend fun listFilesInSourceDirs( + contentResolver: ContentResolver, + titleDir: DocumentFile, + titleUri: Uri, + targetLocation: String, + ): Pair> { + val entries = LinkedList() + var totalSize = 0L + + for (sourceDir in SOURCE_DIRS) { + val parentUri = titleDir.findFile(sourceDir)!!.uri + val parentUriLength = titleUri.toString().length + val uriToTargetPath: (Uri) -> Path = { + val relativePath = it.toString().substring(parentUriLength).urlDecode() + Path(targetLocation, relativePath) + } + + entries += DirEntry.Dir(Path(targetLocation, sourceDir)) + contentResolver.walkDocumentTree( + dirUri = parentUri, + onEntry = { + when (it) { + is DocumentEntry.Directory -> { + entries += DirEntry.Dir(uriToTargetPath(it.uri)) + } + + is DocumentEntry.File -> { + totalSize += it.size + entries += DirEntry.File( + it.uri, + uriToTargetPath(it.uri), + it.size + ) + } + } + }, + ) + } + + totalSize = max(totalSize, 1L) + + return Pair(totalSize, entries) + } + + private fun cleanupInstall(installFile: File) { + val oldCleanupJob = cleanupJob + cleanupJob = scope.launch(Dispatchers.IO) { + oldCleanupJob?.join() + val installPath = installFile.toPath() + var tempFile: File? = null + if (installFile.exists()) { + val tempName = "${installPath.fileName}-${Random.nextUInt()}" + tempFile = installPath.resolveSibling(tempName).toFile() + installFile.renameTo(tempFile!!) + } + val backupInstall = installPath.getBackupFile() + if (backupInstall.exists()) + backupInstall.renameTo(installFile) + tempFile?.deleteRecursively() + } + } + + companion object { + private val SOURCE_DIRS = arrayOf("content", "code", "meta") + private fun Path.getBackupFile() = resolveSibling("$fileName.backup").toFile() + } +} diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml index 7a4dd01a..8c805282 100644 --- a/src/android/app/src/main/res/layout/activity_emulation.xml +++ b/src/android/app/src/main/res/layout/activity_emulation.xml @@ -1,5 +1,5 @@ - - + diff --git a/src/android/app/src/test/java/info/cemu/cemu/tests/ArchitectureTests.kt b/src/android/app/src/test/java/info/cemu/cemu/tests/ArchitectureTests.kt index 1f26fbf8..7e2846db 100644 --- a/src/android/app/src/test/java/info/cemu/cemu/tests/ArchitectureTests.kt +++ b/src/android/app/src/test/java/info/cemu/cemu/tests/ArchitectureTests.kt @@ -49,7 +49,10 @@ class ArchitectureTests { private val isFromTests = resideInAPackage("info.cemu.cemu.tests..") - private fun JavaClasses.thatAreFromMainSources() = that(not(or(isFromGeneratedCode, isFromTests))) + private val isFromNativeInterface = resideInAPackage("info.cemu.cemu.nativeinterface..") + + private fun JavaClasses.thatAreFromMainSources() = + that(not(or(isFromGeneratedCode, isFromTests))) @ArchTest fun `feature packages should not depend on each other`(javaClasses: JavaClasses) { @@ -72,4 +75,14 @@ class ArchitectureTests { .should().dependOnClassesThat(isFromEntryPointCode) .check(javaClasses.thatAreFromMainSources()) } + + @ArchTest + fun `native interface should not depend on any other app packages`(javaClasses: JavaClasses) { + noClasses().that(isFromNativeInterface) + .should().dependOnClassesThat( + resideInAPackage("info.cemu.cemu..") + .and(not(isFromNativeInterface)) + ) + .check(javaClasses.thatAreFromMainSources()) + } } diff --git a/src/android/gradle/libs.versions.toml b/src/android/gradle/libs.versions.toml index 43c13c27..bb6f1ce3 100644 --- a/src/android/gradle/libs.versions.toml +++ b/src/android/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.11.1" +agp = "8.12.1" aboutlibraries = "13.0.0-a01" appcompat = "1.7.1" archunit-junit4 = "1.4.1"